diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..28e0114e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,60 @@ +# Build context for docs/Dockerfile is the monorepo root. +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file + +# Ignore git directory. +/.git/ +/.gitignore + +# Ignore bundler config in either subproject. +/gem/.bundle +/docs/.bundle + +# Ignore environment files. +/docs/.env* +/gem/.env* + +# Ignore default key files. +/docs/config/master.key +/docs/config/credentials/*.key + +# Ignore logs and tempfiles. +/docs/log/* +/docs/tmp/* +!/docs/log/.keep +!/docs/tmp/.keep +/docs/tmp/pids/* +!/docs/tmp/pids/.keep + +# Ignore storage (uploaded files in dev and SQLite databases). +/docs/storage/* +!/docs/storage/.keep +/docs/tmp/storage/* +!/docs/tmp/storage/.keep + +# Ignore assets. +/docs/node_modules/ +/docs/app/assets/builds/* +!/docs/app/assets/builds/.keep +/docs/public/assets + +# Ignore gem build artifacts. +/gem/pkg/ +/gem/node_modules/ +/gem/tmp/ +/gem/.ruby-lsp/ + +# Ignore CI service files. +/.github + +# Ignore Kamal files. +/docs/config/deploy*.yml +/docs/.kamal + +# Ignore development files. +/gem/.devcontainer +/docs/.devcontainer + +# Ignore Docker-related files. +/.dockerignore +/docs/.dockerignore +/docs/Dockerfile* diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f4e660d1..0519090e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @cirdes +* @cirdes @stephannv diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f33a02cd..dfa90497 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,38 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for more information: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates -# https://containers.dev/guide/dependabot - version: 2 updates: - - package-ecosystem: "devcontainers" - directory: "/" - schedule: - interval: weekly + # Gem + - package-ecosystem: "bundler" + directory: "/gem" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + # Docs (Rails app) + - package-ecosystem: "bundler" + directory: "/docs" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + - package-ecosystem: "npm" + directory: "/docs" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + + # GitHub Actions (root) + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 5 + + # Devcontainers + - package-ecosystem: "devcontainers" + directory: "/gem" + schedule: + interval: weekly + - package-ecosystem: "devcontainers" + directory: "/docs" + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6de74e6..1843b32e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,31 +1,86 @@ name: CI on: + pull_request: push: branches: - main - pull_request: jobs: - build: - name: Ruby ${{ matrix.ruby }} + gem: + name: Gem (Ruby ${{ matrix.ruby }}) runs-on: ubuntu-latest + defaults: + run: + working-directory: gem strategy: fail-fast: false matrix: ruby: ["3.3", "3.4"] - steps: - - uses: actions/checkout@v4 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby }} - bundler-cache: true - rubygems: latest + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + rubygems: latest + working-directory: gem + - name: Run tests + run: bundle exec rake test + - name: Run linter + run: bundle exec rake standard - - name: Run tests - run: bundle exec rake test + docs: + name: Docs (Rails) + runs-on: ubuntu-latest + defaults: + run: + working-directory: docs + steps: + - uses: actions/checkout@v6 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + working-directory: docs + - name: Install pnpm + uses: pnpm/action-setup@v5 + with: + version: 10.8.0 + run_install: false + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version-file: docs/.node-version + cache: pnpm + cache-dependency-path: docs/pnpm-lock.yaml + - name: Install dependencies + run: pnpm install + - name: Lint + run: bundle exec standardrb + - name: Run tests + run: bin/rails db:test:prepare test - - name: Run linter - run: bundle exec rake standard + docker-build: + name: Docker build (Devcontainer) + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v6 + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push devcontainer image + uses: devcontainers/ci@v0.3 + with: + imageName: ghcr.io/ruby-ui/web-devcontainer + cacheFrom: ghcr.io/ruby-ui/web-devcontainer + subFolder: docs + push: always diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de6d09d0..b88ff3bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,62 +1,66 @@ # Contributing to RubyUI -Thank you for your interest in contributing to RubyUI! This document provides guidelines for contributing to the project. +Thanks for your interest in contributing! This repository is a monorepo containing two sibling projects: -## Development Setup +- [`gem/`](gem/) — the `ruby_ui` gem published to rubygems.org. +- [`docs/`](docs/) — the Rails app that powers https://rubyui.com. -We recommend using the provided devcontainer to set up your development environment. This ensures a consistent environment for all contributors. +The big advantage of the monorepo: **a single PR can touch both the component and the docs example**. The docs app consumes the local gem via `path: "../gem"`, so any change in `gem/lib/ruby_ui/` is reflected in the running site immediately. -1. Make sure you have Docker -2. Clone the repository -3. Open the project in you editor -4. Select "Reopen in Container" if you are using VSCode or any other method to run the project -5. The devcontainer will set up everything you need to start developing +## Development setup -## Contribution Process +We recommend using the devcontainer for either subproject (each has its own `.devcontainer/`). -1. Fork the repository -2. Create a new branch for your changes -3. Make your changes -4. Run tests to ensure your changes don't break existing functionality: `bundle exec rake test` -5. Run the linter to ensure consistent code formatting: `bundle exec rake standard` -6. Submit a Pull Request to the main repository - -## Focus Areas - -We prioritize: -- Improving existing components rather than adding new ones -- Preserving the shadcn look and feel -- Enhancing documentation -- Fixing bugs +```bash +git clone git@github.com:ruby-ui/ruby_ui.git +cd ruby_ui + +# For gem-only work: +cd gem +bundle install +bundle exec rake + +# For docs work: +cd docs +bundle install +pnpm install +bin/dev +``` -## Code Standards +## Workflow -We follow Standard Ruby conventions for code style. The CI pipeline runs `standard` to ensure consistent code formatting. +1. Fork and create a feature branch. +2. Make your changes: + - Component or generator changes → `gem/lib/...`, with tests in `gem/test/...`. + - Documentation page changes → `docs/app/views/docs/...` or `docs/app/components/...`. + - If a component change affects how it's documented, update **both** in the same PR. +3. Run the relevant test suites: + - `cd gem && bundle exec rake` (tests + standardrb). + - `cd docs && bin/rails test` and `bundle exec standardrb`. +4. Open a Pull Request against `main`. Use the PR template and prefix the title with a category in brackets, e.g. `[Feature] Add new variant to Button`. -## Testing +## Focus areas -While we don't have specific test coverage requirements, all contributions should include tests for new functionality and ensure existing tests pass. +We prioritize: -## Documentation +- Improving existing components rather than adding new ones. +- Preserving the shadcn look and feel. +- Enhancing documentation. +- Fixing bugs. -If your changes include new components, modify how components should be used, or add new behaviors, it is highly recommended to also open a PR on the [ruby-ui/web](https://github.com/ruby-ui/web) repository. This ensures the documentation website stays up-to-date with the latest component changes. +## Code style -### Installing Documentation Files +- Ruby: [Standard Ruby](https://github.com/standardrb/standard) — `bundle exec standardrb --fix` to auto-fix. +- JavaScript: kept minimal; Stimulus controllers live alongside the components they belong to. -RubyUI includes documentation files for each component that can be installed into your Rails application. These files are located at `lib/ruby_ui/{component}/{component}_docs.rb` and provide usage examples for each component. +## Documentation files -To install the documentation files: +The gem ships per-component `*_docs.rb` files (rendering examples) under `gem/lib/ruby_ui//`. Consumers of the gem can install these into their own Rails app with: ```bash bin/rails g ruby_ui:install:docs ``` -To overwrite existing documentation files: - -```bash -bin/rails g ruby_ui:install:docs --force -``` - -This will copy the documentation files to `app/views/docs/` in your Rails application. +Within this monorepo, the **docs app does not run that generator** — it has its own richer view implementations in `docs/app/views/docs/` and `docs/app/components/docs/`. If you change a component's API, update the relevant view in `docs/app/views/docs/.rb` to keep the documentation site accurate. -Thank you for contributing to make RubyUI better! \ No newline at end of file +Thanks for helping make RubyUI better! diff --git a/README.md b/README.md index e3e91119..770a73f3 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,51 @@ -# RubyUI (former PhlexUI) 🚀 +# RubyUI -Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source. - -This is NOT a component library. It's a collection of re-usable components that you can generate or copy and paste into your apps. - -Pick the components you need. Copy and paste the code into your project and customize to your needs. The code is yours. - -Use this as a reference to build your own component libraries. - -### Key Features: - -- **Built for Speed** ⚡: RubyUI leverages Phlex, which is up to 12x faster than traditional Rails ERB templates. -- **Stunning UI** 🎨: Design beautiful, streamlined, and customizable UIs that sell your app effortlessly. -- **Stay Organized** 📁: Keep your UI components well-organized and easy to manage. -- **Customer-Centric UX** 🧑‍💼: Create memorable app experiences for your users. -- **Completely Customizable** 🔧: Full control over the design of all components. -- **Minimal Dependencies** 🍃: Uses custom-built Stimulus.js controllers to keep your app lean. -- **Reuse with Ease** ♻️: Build components once and use them seamlessly across your project. - -### How to Use: +[![CI](https://github.com/ruby-ui/ruby_ui/actions/workflows/ci.yml/badge.svg)](https://github.com/ruby-ui/ruby_ui/actions/workflows/ci.yml) +[![Gem Version](https://badge.fury.io/rb/ruby_ui.svg)](https://rubygems.org/gems/ruby_ui) -1. **Find the perfect component** 🔍: Browse live-embedded components on our documentation page. -2. **Copy the snippet** 📋: Easily copy code snippets for quick implementation. -3. **Make it yours** 🎨: Customize components using Tailwind utility classes to fit your specific needs. - -## Installation 🚀 - -> [!NOTE] -> RubyUI 1.0 requires Ruby 3.2 or later +Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source. -### 1. Install the gem +This repository is a **monorepo** with two sibling projects: -```bash -bundle add ruby_ui --group development --require false +``` +ruby_ui/ +├── gem/ # the ruby_ui gem (lib/, generators, tests, gemspec) +└── docs/ # the Rails app that powers https://rubyui.com ``` -or add it to your Gemfile: +## Quick links -```ruby -gem "ruby_ui", group: :development, require: false -``` +- **Use the gem in your app:** see [`gem/README.md`](gem/README.md). +- **Documentation site:** https://rubyui.com/docs/introduction +- **Contributing guide:** [`CONTRIBUTING.md`](CONTRIBUTING.md) -### 2. Run the installer: +## Layout -```bash -bin/rails g ruby_ui:install -``` +| Path | What lives here | +| --- | --- | +| [`gem/`](gem/) | The `ruby_ui` gem (`gem build`, `gem release` from this folder). | +| [`docs/`](docs/) | Rails 8 app for the documentation site. Consumes the local gem via `path: "../gem"`. | +| [`.github/workflows/ci.yml`](.github/workflows/ci.yml) | Unified CI: gem tests on Ruby 3.3 + 3.4, Rails docs app tests, and a docker-build job that publishes the docs devcontainer to ghcr.io. | -### 3. Done! 🎉 +## Development -You can generate your components using `ruby_ui:component` generator. +The two projects are independent for everyday work — pick the one you need: ```bash -bin/rails g ruby_ui:component Accordion +# Gem work +cd gem +bundle install +bundle exec rake # tests + standardrb + +# Docs work (consumes the local gem via path: "../gem") +cd docs +bundle install +pnpm install +bin/dev # http://localhost:3000 ``` -You also can generate all components using `ruby_ui:component:all` generator - -## Documentation 📖 - -Visit https://rubyui.com/docs/introduction to view the full documentation, including: - -- Detailed component guides -- Themes -- Lookbook -- Getting started guide - -## Speed Comparison 🏎️ - -RubyUI, powered by Phlex, outperforms alternative methods: - -- Phlex: Baseline 🏁 -- ViewComponent: ~1.5x slower 🚙 -- ERB Templates: ~5x slower 🐢 - -See the original [view layers benchmark](https://github.com/KonnorRogers/view-layer-benchmarks) by @KonnorRogers and its [variations](https://github.com/KonnorRogers/view-layer-benchmarks/forks). - -## Importmap notes: - -If you run into importmap issues this stackoverflow question might help: -https://stackoverflow.com/questions/70548841/how-to-add-custom-js-file-to-new-rails-7-project/72855705 - -## License 📜 - -Licensed under the [MIT license](https://github.com/shadcn/ui/blob/main/LICENSE.md). - ---- - -## Sponsors -[![DigitalOcean Referral Badge](https://web-platforms.sfo2.cdn.digitaloceanspaces.com/WWW/Badge%201.svg)](https://www.digitalocean.com/?refcode=0fdaefc76c39&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge) +Editing files under `gem/lib/ruby_ui/` is reflected immediately when running the docs app — no `bundle update`, no rebuild, no PR coordination across two repos. +## License -© 2024 RubyUI. All rights reserved. 🔒 +Released under the [MIT License](gem/LICENSE.txt). diff --git a/.devcontainer/Dockerfile b/docs/.devcontainer/Dockerfile similarity index 100% rename from .devcontainer/Dockerfile rename to docs/.devcontainer/Dockerfile diff --git a/docs/.devcontainer/compose.yaml b/docs/.devcontainer/compose.yaml new file mode 100644 index 00000000..b891aefe --- /dev/null +++ b/docs/.devcontainer/compose.yaml @@ -0,0 +1,25 @@ +name: "rubyui-web" + +services: + rails-app: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + + volumes: + - ../../web:/workspaces/web:cached + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + + # Uncomment the next line to use a non-root user for all processes. + # user: vscode + + # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + depends_on: + - selenium + + selenium: + image: selenium/standalone-chromium + restart: unless-stopped diff --git a/docs/.devcontainer/devcontainer.json b/docs/.devcontainer/devcontainer.json new file mode 100644 index 00000000..d6e9eeee --- /dev/null +++ b/docs/.devcontainer/devcontainer.json @@ -0,0 +1,43 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/ruby +{ + "name": "rubyui-web", + "dockerComposeFile": "compose.yaml", + "service": "rails-app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/rails/devcontainer/features/activestorage": {}, + "ghcr.io/devcontainers/features/node:1": { "version": 20, "pnpmVersion": "10.8.0" }, + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {"moby":false}, + "ghcr.io/rails/devcontainer/features/sqlite3": {} + }, + + "containerEnv": { + "CAPYBARA_SERVER_PORT": "45678", + "SELENIUM_HOST": "selenium" + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 6379], + + // Configure tool-specific properties. + // "customizations": {}, + "customizations": { + "vscode": { + "extensions": [ + "Shopify.ruby-lsp", + "testdouble.vscode-standard-ruby" + ] + } + }, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root", + + + // Use 'postCreateCommand' to run commands after the container is created. + "onCreateCommand": "bin/setup --skip-server" +} diff --git a/docs/.gitattributes b/docs/.gitattributes new file mode 100644 index 00000000..31eeee0b --- /dev/null +++ b/docs/.gitattributes @@ -0,0 +1,7 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..011574f2 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,48 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore uploaded files in development. +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets + +# Ignore master key for decrypting credentials and more. +/config/master.key + +/app/assets/builds/* +!/app/assets/builds/.keep + +/node_modules + +.env* + +.tool-versions + +config/credentials/production.key + +# Yarn +yarn-error.log + +# Pnpm +.pnpm-store diff --git a/docs/.node-version b/docs/.node-version new file mode 100644 index 00000000..d5a15960 --- /dev/null +++ b/docs/.node-version @@ -0,0 +1 @@ +20.10.0 diff --git a/docs/.ruby-version b/docs/.ruby-version new file mode 100644 index 00000000..2aa51319 --- /dev/null +++ b/docs/.ruby-version @@ -0,0 +1 @@ +3.4.7 diff --git a/docs/.standard.yml b/docs/.standard.yml new file mode 100644 index 00000000..f81b9a47 --- /dev/null +++ b/docs/.standard.yml @@ -0,0 +1,2 @@ +ignore: + - 'db/migrate/*' \ No newline at end of file diff --git a/docs/.vscode/settings.json b/docs/.vscode/settings.json new file mode 100644 index 00000000..688f7bd2 --- /dev/null +++ b/docs/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.detectIndentation": false, + "editor.tabSize": 2, + "[ruby]": { + "editor.formatOnSave": true, + }, +} \ No newline at end of file diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 00000000..a6fce0a1 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1 @@ +@CLAUDE.md \ No newline at end of file diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 00000000..dacf5627 --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,99 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# Build context for this Dockerfile is the monorepo root, not docs/. +# Run from the repo root: +# docker build -f docs/Dockerfile . +# flyctl deploy --config docs/fly.toml --dockerfile docs/Dockerfile . + +# Make sure RUBY_VERSION matches the Ruby version in docs/.ruby-version +ARG RUBY_VERSION=3.4.7 +FROM quay.io/evl.ms/fullstaq-ruby:${RUBY_VERSION}-jemalloc-slim AS base + +LABEL fly_launch_runtime="rails" + +# Rails app lives here +WORKDIR /rails + +# Update gems and bundler +RUN gem update --system --no-document && \ + gem install -N bundler + +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl sqlite3 && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment +ENV BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development:test" \ + RAILS_ENV="production" + + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build gems and node modules +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git libyaml-dev node-gyp pkg-config python-is-python3 && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install Node.js +ARG NODE_VERSION=20.10.0 +ARG PNPM_VERSION=10.8.0 +ENV PATH=/usr/local/node/bin:$PATH +RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \ + /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \ + npm install -g pnpm@$PNPM_VERSION && \ + rm -rf /tmp/node-build-master + +# Copy the gem first so docs/Gemfile's `path: "../gem"` resolves during bundle install. +COPY gem /gem + +# Install application gems (cwd = /rails) +COPY docs/Gemfile docs/Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile + +# Install node modules +COPY docs/package.json docs/pnpm-lock.yaml ./ +RUN pnpm install + +# Copy application code +COPY docs/ ./ + +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + +# Final stage for app image +FROM base + + +# Copy built artifacts: gems, application, and the gem subtree +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /gem /gem +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + mkdir /data && \ + chown -R 1000:1000 db log storage tmp /data +USER 1000:1000 + +# Deployment options +ENV DATABASE_URL="sqlite3:///data/production.sqlite3" + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +VOLUME /data +CMD ["./bin/rails", "server"] diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 00000000..fd65ad8e --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,83 @@ +source "https://rubygems.org" +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +ruby "3.4.7" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "8.1.3" +# The modern asset pipeline for Rails [https://github.com/rails/propshaft] +gem "propshaft", "1.3.2" +# Use sqlite3 as the database for Active Record +gem "sqlite3", "2.9.3" +# Use the Puma web server [https://github.com/puma/puma] +gem "puma", "7.2.0" +# Bundle and transpile JavaScript [https://github.com/rails/jsbundling-rails] +gem "jsbundling-rails", "1.3.1" +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] +gem "turbo-rails", "2.0.23" +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] +gem "stimulus-rails", "1.3.4" +# Bundle and process CSS [https://github.com/rails/cssbundling-rails] +gem "cssbundling-rails", "1.4.3" + +gem "lucide-rails", "0.7.4" + +# gem "jbuilder" + +# Use Redis adapter to run Action Cable in production +# gem "redis", "~> 4.0" + +# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] +# gem "kredis" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +# gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false + +# Use Sass to process CSS +# gem "sassc-rails" + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +# gem "image_processing", "~> 1.2" + +# gem "dotenv-rails", groups: [:development, :test] + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[mri mingw x64_mingw] +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" + + # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] + # gem "rack-mini-profiler" + + # Speed up commands on slow machines / big apps [https://github.com/rails/spring] + # gem "spring" + gem "standard" +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" +end + +gem "phlex", github: "phlex-ruby/phlex" +gem "phlex-rails", github: "phlex-ruby/phlex-rails" + +gem "ruby_ui", path: "../gem", require: false + +gem "pry", "0.16.0" + +gem "tailwind_merge", "~> 1.4.0" +gem "rss", "0.3.2" + +gem "rouge", "~> 4.7" diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 00000000..58048468 --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,360 @@ +GIT + remote: https://github.com/phlex-ruby/phlex-rails.git + revision: 2a0078767af4feea43e0772620e1d88529dc4d1a + specs: + phlex-rails (2.0.2) + phlex (~> 2.0.2) + railties (>= 6.1, < 9) + +GIT + remote: https://github.com/phlex-ruby/phlex.git + revision: 8c5938db3254690c423ba42c63c7f8631f540c79 + specs: + phlex (2.0.2) + +PATH + remote: ../gem + specs: + ruby_ui (1.1.0) + +GEM + remote: https://rubygems.org/ + specs: + action_text-trix (2.1.18) + railties + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.3) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.3) + activesupport (= 8.1.3) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.3) + activesupport (= 8.1.3) + globalid (>= 0.3.6) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) + timeout (>= 0.4.0) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) + marcel (~> 1.0) + activesupport (8.1.3) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ast (2.4.3) + base64 (0.3.0) + bigdecimal (4.1.2) + bindex (0.8.1) + bootsnap (1.23.0) + msgpack (~> 1.2) + builder (3.3.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + coderay (1.1.3) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + crass (1.0.6) + cssbundling-rails (1.4.3) + railties (>= 6.0.0) + date (3.5.1) + debug (1.11.1) + irb (~> 1.10) + reline (>= 0.3.8) + drb (2.2.3) + erb (6.0.2) + erubi (1.13.1) + globalid (1.3.0) + activesupport (>= 6.1) + i18n (1.14.8) + concurrent-ruby (~> 1.0) + io-console (0.8.2) + irb (1.17.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jsbundling-rails (1.3.1) + railties (>= 6.0.0) + json (2.19.4) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.25.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + lucide-rails (0.7.4) + railties (>= 4.1.0) + mail (2.9.0) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.1.0) + matrix (0.4.2) + method_source (1.1.0) + mini_mime (1.1.5) + mini_portile2 (2.8.9) + minitest (6.0.4) + drb (~> 2.0) + prism (~> 1.5) + msgpack (1.8.0) + net-imap (0.6.3) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.1) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.2) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.19.2-x86_64-linux-gnu) + racc (~> 1.4) + parallel (1.27.0) + parser (3.3.10.1) + ast (~> 2.4.1) + racc + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + propshaft (1.3.2) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + pry (0.16.0) + coderay (~> 1.1) + method_source (~> 1.0) + reline (>= 0.6.0) + psych (5.3.1) + date + stringio + public_suffix (6.0.1) + puma (7.2.0) + nio4r (~> 2.0) + racc (1.8.1) + rack (3.2.6) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) + bundler (>= 1.15.0) + railties (= 8.1.3) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.3.1) + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort + regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) + rexml (3.4.4) + rouge (4.7.0) + rss (0.3.2) + rexml + rubocop (1.84.2) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.0) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-progressbar (1.13.0) + rubyzip (3.2.2) + securerandom (0.4.1) + selenium-webdriver (4.43.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) + sin_lru_redux (2.5.2) + sqlite3 (2.9.3) + mini_portile2 (~> 2.8.0) + sqlite3 (2.9.3-x86_64-linux-gnu) + standard (1.54.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.84.0) + standard-custom (~> 1.0.0) + standard-performance (~> 1.8) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.9.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.26.0) + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.2.0) + tailwind_merge (1.4.0) + sin_lru_redux (~> 2.5) + thor (1.5.0) + timeout (0.6.1) + tsort (0.2.0) + turbo-rails (2.0.23) + actionpack (>= 7.1.0) + railties (>= 7.1.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + useragent (0.16.11) + web-console (4.3.0) + actionview (>= 8.0.0) + bindex (>= 0.4.0) + railties (>= 8.0.0) + websocket (1.2.11) + websocket-driver (0.8.0) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.7.5) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + bootsnap + capybara + cssbundling-rails (= 1.4.3) + debug + jsbundling-rails (= 1.3.1) + lucide-rails (= 0.7.4) + phlex! + phlex-rails! + propshaft (= 1.3.2) + pry (= 0.16.0) + puma (= 7.2.0) + rails (= 8.1.3) + rouge (~> 4.7) + rss (= 0.3.2) + ruby_ui! + selenium-webdriver + sqlite3 (= 2.9.3) + standard + stimulus-rails (= 1.3.4) + tailwind_merge (~> 1.4.0) + turbo-rails (= 2.0.23) + tzinfo-data + web-console + +RUBY VERSION + ruby 3.4.7p58 + +BUNDLED WITH + 2.6.4 diff --git a/docs/Procfile b/docs/Procfile new file mode 100644 index 00000000..e52f7ba0 --- /dev/null +++ b/docs/Procfile @@ -0,0 +1,3 @@ +web: bin/rails server +release: rails db:migrate + diff --git a/docs/Procfile.dev b/docs/Procfile.dev new file mode 100644 index 00000000..5b066621 --- /dev/null +++ b/docs/Procfile.dev @@ -0,0 +1,3 @@ +web: env RUBY_DEBUG_OPEN=true bin/rails server +js: pnpm build --watch +css: pnpm build:css --watch diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..fb1e72a7 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,28 @@ +Rails need a plug n play system for creating streamlined ui components. + +Phlex looks fun and fast, so I thought I'd start creating ui components with it. + +## Contributing - Local Development Setup + +### Install the Gem Locally + +To contribute to this project, it's recommended to install the gem locally and point to it in your Gemfile: + +```ruby +gem "ruby_ui", path: "../ruby_ui" +``` + +## Working with Components + +### Component Development Workflow + +1. Eject the component you want to modify using the generator: + ```bash + rails generate ruby_ui:component combobox + ``` +2. Make your desired changes to the ejected component +3. Once you're satisfied with the modifications, integrate the component back into the gem in the appropriate location + +This workflow allows you to iterate quickly on components while maintaining the gem's structure. + +Would you like me to expand on any part of the contributing guide? diff --git a/docs/Rakefile b/docs/Rakefile new file mode 100644 index 00000000..9a5ea738 --- /dev/null +++ b/docs/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/docs/app/.DS_Store b/docs/app/.DS_Store new file mode 100644 index 00000000..6a63f007 Binary files /dev/null and b/docs/app/.DS_Store differ diff --git a/docs/app/assets/.DS_Store b/docs/app/assets/.DS_Store new file mode 100644 index 00000000..6d489ef2 Binary files /dev/null and b/docs/app/assets/.DS_Store differ diff --git a/docs/app/assets/builds/.keep b/docs/app/assets/builds/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/app/assets/config/manifest.js b/docs/app/assets/config/manifest.js new file mode 100644 index 00000000..5eee9184 --- /dev/null +++ b/docs/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_tree ../animations +//= link_tree ../builds diff --git a/docs/app/assets/images/.keep b/docs/app/assets/images/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/app/assets/images/email_sent.jpg b/docs/app/assets/images/email_sent.jpg new file mode 100644 index 00000000..ee86f7bc Binary files /dev/null and b/docs/app/assets/images/email_sent.jpg differ diff --git a/docs/app/assets/images/email_sent.png b/docs/app/assets/images/email_sent.png new file mode 100644 index 00000000..9700c894 Binary files /dev/null and b/docs/app/assets/images/email_sent.png differ diff --git a/docs/app/assets/images/email_sent.svg b/docs/app/assets/images/email_sent.svg new file mode 100644 index 00000000..c57af001 --- /dev/null +++ b/docs/app/assets/images/email_sent.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/app/assets/images/george_computer.jpg b/docs/app/assets/images/george_computer.jpg new file mode 100644 index 00000000..8f593479 Binary files /dev/null and b/docs/app/assets/images/george_computer.jpg differ diff --git a/docs/app/assets/images/george_lewagon.jpg b/docs/app/assets/images/george_lewagon.jpg new file mode 100644 index 00000000..5f77aaa6 Binary files /dev/null and b/docs/app/assets/images/george_lewagon.jpg differ diff --git a/docs/app/assets/images/george_nature.jpg b/docs/app/assets/images/george_nature.jpg new file mode 100644 index 00000000..a5139ae6 Binary files /dev/null and b/docs/app/assets/images/george_nature.jpg differ diff --git a/docs/app/assets/images/installation_blur.jpg b/docs/app/assets/images/installation_blur.jpg new file mode 100644 index 00000000..1506caf0 Binary files /dev/null and b/docs/app/assets/images/installation_blur.jpg differ diff --git a/docs/app/assets/images/logo.svg b/docs/app/assets/images/logo.svg new file mode 100644 index 00000000..a5628dbc --- /dev/null +++ b/docs/app/assets/images/logo.svg @@ -0,0 +1,51 @@ + + + + + + + + RubyUI + + diff --git a/docs/app/assets/images/logo_dark.svg b/docs/app/assets/images/logo_dark.svg new file mode 100644 index 00000000..30e7877e --- /dev/null +++ b/docs/app/assets/images/logo_dark.svg @@ -0,0 +1,51 @@ + + + + + + + + RubyUI + + diff --git a/docs/app/assets/images/pattern.jpg b/docs/app/assets/images/pattern.jpg new file mode 100644 index 00000000..f46c2056 Binary files /dev/null and b/docs/app/assets/images/pattern.jpg differ diff --git a/docs/app/assets/stylesheets/application.tailwind.css b/docs/app/assets/stylesheets/application.tailwind.css new file mode 100644 index 00000000..67fc2752 --- /dev/null +++ b/docs/app/assets/stylesheets/application.tailwind.css @@ -0,0 +1,161 @@ +@import "tailwindcss"; + +@plugin "@tailwindcss/forms"; +@plugin "@tailwindcss/typography"; + + +@import "tw-animate-css"; + + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + + /* ruby_ui specific */ + --warning: hsl(38 92% 50%); + --warning-foreground: hsl(0 0% 100%); + --success: hsl(87 100% 37%); + --success-foreground: hsl(0 0% 100%); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); + + /* ruby_ui specific */ + --warning: hsl(38 92% 50%); + --warning-foreground: hsl(0 0% 100%); + --success: hsl(84 81% 44%); + --success-foreground: hsl(0 0% 100%); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + /* ruby_ui specific */ + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + + /* Fonts */ + --font-sans: "Inter", "system-ui", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "sans-serif"; + --font-heading: "Inter", "system-ui", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "sans-serif"; +} + +/* Container settings */ +@utility container { + margin-inline: auto; + padding-inline: 2rem; + max-width: 1400px; +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground font-sans antialiased; + } + h1, h2, h3, h4, h5, h6 { + @apply tracking-tight; + } +} diff --git a/docs/app/channels/application_cable/channel.rb b/docs/app/channels/application_cable/channel.rb new file mode 100644 index 00000000..d6726972 --- /dev/null +++ b/docs/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/docs/app/channels/application_cable/connection.rb b/docs/app/channels/application_cable/connection.rb new file mode 100644 index 00000000..0ff5442f --- /dev/null +++ b/docs/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/docs/app/components/base.rb b/docs/app/components/base.rb new file mode 100644 index 00000000..c8d20891 --- /dev/null +++ b/docs/app/components/base.rb @@ -0,0 +1,35 @@ +class Components::Base < Phlex::HTML + include Components + include RubyUI + + # Include any helpers you want to be available across all components + include Phlex::Rails::Helpers::Routes + include Phlex::Rails::Helpers::ImagePath + include Phlex::Rails::Helpers::ImageURL + include Phlex::Rails::Helpers::Flash + include Phlex::Rails::Helpers::Request + include Phlex::Rails::Helpers::ContentTag + include LucideRails::RailsHelper + + TAILWIND_MERGER = ::TailwindMerge::Merger.new.freeze unless defined?(TAILWIND_MERGER) + + attr_reader :attrs + + def initialize(**user_attrs) + @attrs = mix(default_attrs, user_attrs) + @attrs[:class] = TAILWIND_MERGER.merge(@attrs[:class]) if @attrs[:class] + end + + if Rails.env.development? + def before_template + comment { "Before #{self.class.name}" } + super + end + end + + private + + def default_attrs + {} + end +end diff --git a/docs/app/components/component_setup/cli_steps.rb b/docs/app/components/component_setup/cli_steps.rb new file mode 100644 index 00000000..22297a47 --- /dev/null +++ b/docs/app/components/component_setup/cli_steps.rb @@ -0,0 +1,30 @@ +module Components + module ComponentSetup + class CLISteps < Components::Base + def initialize(component_name:) + @component_name = component_name + end + + private + + attr_reader :component_name + + def view_template + div(class: "max-w-2xl mx-auto w-full py-10 space-y-6") do + Heading(level: 4, class: "pb-4 border-b") { "Using RubyUI CLI" } + + Text(size: "4", weight: "semibold") do + "Run the install command" + end + + code = <<~CODE + rails g ruby_ui:component #{component_name.camelcase} + CODE + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + end + end + end + end +end diff --git a/docs/app/components/component_setup/manual_steps.rb b/docs/app/components/component_setup/manual_steps.rb new file mode 100644 index 00000000..0019100d --- /dev/null +++ b/docs/app/components/component_setup/manual_steps.rb @@ -0,0 +1,197 @@ +module Components + module ComponentSetup + class ManualSteps < Components::Base + def initialize(component_name:) + @component_name = component_name + @dependencies = RubyUI::FileManager.dependencies(@component_name) + end + + private + + attr_reader :component_name, :dependencies + + def view_template + div(class: "max-w-2xl mx-auto w-full py-10 space-y-6") do + Heading(level: 4, class: "pb-4 border-b") { "Manual installation" } + + render Steps::Builder.new do |steps| + component_steps(steps) + stimulus_controller_steps(steps) + js_dependencies_steps(steps) + ruby_dependencies_steps(steps) + component_dependencies_steps(steps) + end + end + end + + def component_steps(steps) + component_file_paths = RubyUI::FileManager.component_file_paths(component_name) + + component_file_paths.each do |component_path| + component_class = component_path.split("/").last.delete_suffix(".rb").camelcase + component_file_name = component_path.split("/").last + component_code = RubyUI::FileManager.component_code(component_path) + + steps.add_step do + render Steps::Container do + Text(size: "4", weight: "semibold") do + plain "Add " + InlineCode(class: "whitespace-nowrap") { "RubyUI::#{component_class}" } + plain " to " + InlineCode(class: "whitespace-nowrap") { "app/components/ruby_ui/#{component_name.underscore}/#{component_file_name}" } + end + + div(class: "w-full") do + Codeblock(component_code, syntax: :ruby) + end + end + end + end + end + + def stimulus_controller_steps(steps) + stimulus_controller_file_paths = RubyUI::FileManager.stimulus_controller_file_paths(component_name) + + return if stimulus_controller_file_paths.empty? + + stimulus_controller_file_paths.each do |controller_path| + controller_file_name = controller_path.split("/").last + controller_code = RubyUI::FileManager.component_code(controller_path) + steps.add_step do + render Steps::Container do + Text(size: "4", weight: "semibold") do + plain "Add " + InlineCode(class: "whitespace-nowrap") { controller_file_name } + plain " to " + InlineCode(class: "whitespace-nowrap") { "app/javascript/controllers/ruby_ui/#{controller_file_name}" } + end + + div(class: "w-full") do + Codeblock(controller_code, syntax: :javascript) + end + end + end + end + + steps.add_step do + render Steps::Container do + Text(size: "4", weight: "semibold") do + plain "Update the Stimulus controllers manifest file" + end + + Alert(variant: :destructive) do + AlertTitle { "Importmap!" } + AlertDescription { "You don't need to run this command if you are using Importmap" } + end + + div(class: "w-full") do + Codeblock("rake stimulus:manifest:update", syntax: :javascript) + end + end + end + end + + def js_dependencies_steps(steps) + return unless dependencies["js_packages"].present? + + dependencies["js_packages"].each do |js_package| + steps.add_step do + code = <<~CODE + // with yarn + yarn add #{js_package} + // with npm + npm install #{js_package} + // with importmaps + #{pin_importmap_instructions(js_package)} + CODE + + render Steps::Container do + Text(size: "4", weight: "semibold") do + plain "Install " + InlineCode(class: "whitespace-nowrap") { js_package } + plain " Javascript dependency" + end + + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + end + end + end + end + + def ruby_dependencies_steps(steps) + return unless dependencies["gems"].present? + + dependencies["gems"].each do |gem| + steps.add_step do + code = <<~CODE + bundle add #{gem} + CODE + + render Steps::Container do + Text(size: "4", weight: "semibold") do + plain "Install " + InlineCode(class: "whitespace-nowrap") { gem } + plain " Ruby gem" + end + + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + end + end + end + end + + def component_dependencies_steps(steps) + return unless dependencies["components"].present? + + steps.add_step do + render Steps::Container do + Text(size: "4", weight: "semibold") do + plain "Install required components" + end + + Text do + plain "Component " + InlineCode(class: "whitespace-nowrap") { component_name.camelcase } + plain " relies on the following RubyUI components. Refer to their individual installation guides for setup instructions:" + end + + TypographyList do + dependencies["components"].each do |component| + TypographyListItem do + Link(size: :lg, target: "_blank", href: public_send(:"docs_#{component.underscore}_path")) do + span(class: "font-bold") { component.camelcase } + span { " - Installation guide" } + end + end + end + end + end + end + end + + # Temporary solution while we don't remove + # motion adn tippy.js dependencies + def pin_importmap_instructions(js_package) + case js_package + when "motion" + <<~CODE + // Add to your config/importmap.rb + pin "motion", to: "https://cdn.jsdelivr.net/npm/motion@10.18.0/+esm" + CODE + when "tippy.js" + <<~CODE + // Add to your config/importmap.rb + pin "tippy.js", to: "https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/+esm" + pin "@popperjs/core", to: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/+esm"\n + CODE + else + "bin/importmap pin #{js_package}" + end + end + end + end +end diff --git a/docs/app/components/component_setup/tabs.rb b/docs/app/components/component_setup/tabs.rb new file mode 100644 index 00000000..822816f4 --- /dev/null +++ b/docs/app/components/component_setup/tabs.rb @@ -0,0 +1,32 @@ +module Components + module ComponentSetup + class Tabs < Components::Base + def initialize(component_name:) + @component_name = component_name + end + + private + + attr_reader :component_name + + def view_template + Heading(level: 2) { "Installation" } + + RubyUI::Tabs(default_value: "cli", class: "w-full") do + TabsList do + TabsTrigger(value: "cli") { "CLI" } + TabsTrigger(value: "manual") { "Manual" } + end + + TabsContent(value: "cli") do + render CLISteps.new(component_name:) + end + + TabsContent(value: "manual") do + render ManualSteps.new(component_name:) + end + end + end + end + end +end diff --git a/docs/app/components/docs/components_table.rb b/docs/app/components/docs/components_table.rb new file mode 100644 index 00000000..559b9c88 --- /dev/null +++ b/docs/app/components/docs/components_table.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Components + module Docs + class ComponentsTable < Components::Base + def initialize(component_files) + @component_files = component_files.sort_by { |component| [component.built_using, component.name] } + end + + def view_template + Heading(level: 2) { "Components" } + + component_table_view(@component_files) + end + + private + + def component_table_view(components) + div(class: "border rounded-lg overflow-hidden") do + Table do + TableHeader do + TableRow do + div(class: "flex items-center space-x-2") do + TableHead { "Component" } + end + TableHead(class: "w-full grow-1") { "Built using" } + TableHead(class: "text-right") { "Source" } + end + end + + TableBody do + components.each do |component| + TableRow do + TableCell do + InlineCode { component.name } + end + TableCell do + component.built_using + case component.built_using&.to_sym + when :phlex + Badge(size: :sm, variant: :rose) { "Phlex" } + when :stimulus + Badge(size: :sm, variant: :amber) { "Stimulus JS" } + end + end + TableCell do + div(class: "flex justify-end") do + Link(href: component.source, variant: :outline, size: :sm, target: "_blank") do + github_icon + span(class: "ml-2") { "See source" } + end + end + end + end + end + end + end + end + end + + def github_icon + svg(viewbox: "0 0 438.549 438.549", class: "h-4 w-4") do |s| + s.path( + fill: "currentColor", + d: + "M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z" + ) + end + end + end + end +end diff --git a/docs/app/components/docs/header.rb b/docs/app/components/docs/header.rb new file mode 100644 index 00000000..45433952 --- /dev/null +++ b/docs/app/components/docs/header.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Components + module Docs + class Header < Components::Base + def initialize(title: nil, description: nil) + @title = title + @description = description + end + + def view_template + div(class: "space-y-2") do + h1(class: "scroll-m-24 text-3xl font-semibold tracking-tight sm:text-3xl") { @title } + p(class: "text-lg text-foreground") { @description } + end + end + + private + + def alert_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" + ) + end + end + end + end +end diff --git a/docs/app/components/docs/tailwind_config.rb b/docs/app/components/docs/tailwind_config.rb new file mode 100644 index 00000000..40d9cd66 --- /dev/null +++ b/docs/app/components/docs/tailwind_config.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +module Components + module Docs + class TailwindConfig < Components::Base + def view_template + Text(size: "4", weight: "semibold") { "Update Tailwind Configuration" } + Text do + plain "Add the following to your " + InlineCode(class: "whitespace-nowrap") { "tailwind.config.js" } + plain " file" + end + Codeblock(tailwind_config, syntax: :javascript) + end + + private + + def tailwind_config + <<~CODE + // For importing tailwind styles from ruby_ui gem + const execSync = require('child_process').execSync; + + // Import ruby_ui gem path (To make sure Tailwind loads classes used by ruby_ui gem) + const outputRUBYUI = execSync('bundle show ruby_ui', { encoding: 'utf-8' }); + const ruby_ui_path = outputRUBYUI.trim() + '/**/*.rb'; + + const defaultTheme = require('tailwindcss/defaultTheme') + + module.exports = { + darkMode: ["class"], + content: [ + './app/views/**/*.{erb,haml,html,slim,rb}', + './app/helpers/**/*.rb', + './app/assets/stylesheets/**/*.css', + './app/javascript/**/*.js', + ruby_ui_path + ], + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + warning: { + DEFAULT: "hsl(var(--warning))", + foreground: "hsl(var(--warning-foreground))", + }, + success: { + DEFAULT: "hsl(var(--success))", + foreground: "hsl(var(--success-foreground))", + }, + }, + borderRadius: { + lg: `var(--radius)`, + md: `calc(var(--radius) - 2px)`, + sm: "calc(var(--radius) - 4px)", + }, + fontFamily: { + sans: ["var(--font-sans)", ...defaultTheme.fontFamily.sans], + }, + }, + }, + # Not compatible with importmaps + plugins: [ + require("tailwindcss-animate"), + ], + } + CODE + end + end + end +end diff --git a/docs/app/components/docs/tailwind_css.rb b/docs/app/components/docs/tailwind_css.rb new file mode 100644 index 00000000..38d179c4 --- /dev/null +++ b/docs/app/components/docs/tailwind_css.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Components + module Docs + class TailwindCss < Components::Base + def view_template + Text(size: "4", weight: "semibold") { "Add CSS variables" } + Text do + plain "Add the following to your " + InlineCode { "app/assets/stylesheets/application.tailwind.css" } + plain " file" + end + code = css_variables + Codeblock(code, syntax: :css) + end + + private + + def css_variables + <<~CODE + @tailwind base; + @tailwind components; + @tailwind utilities; + + + @layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 5.9% 10%; + --radius: 0.5rem; + --warning: 38 92% 50%; + --warning-foreground: 0 0% 100%; + --success: 87 100% 37%; + --success-foreground: 0 0% 100%; + } + + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --warning: 38 92% 50%; + --warning-foreground: 0 0% 100%; + --success: 84 81% 44%; + --success-foreground: 0 0% 100%; + } + } + + @layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + font-feature-settings: "rlig" 1, "calt" 1; + } + } + CODE + end + end + end +end diff --git a/docs/app/components/docs/visual_code_example.rb b/docs/app/components/docs/visual_code_example.rb new file mode 100644 index 00000000..9f8f2757 --- /dev/null +++ b/docs/app/components/docs/visual_code_example.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +module Components + module Docs + class VisualCodeExample < Components::Base + @@collected_code = [] + + def self.collected_code + @@collected_code.join("\n") + end + + def self.reset_collected_code + @@collected_code = [] + end + + def initialize(title: nil, description: nil, src: nil, context: nil) + @title = title + @description = description + @src = src + @context = context + end + + def view_template(&) + @display_code = CGI.unescapeHTML(capture(&)) + @@collected_code << @display_code + + div(id: @title) do + div(class: "relative") do + Tabs(default_value: "preview") do + div(class: "flex justify-between items-end mb-4 gap-x-2") do + render_header + div(class: "flex-grow") # Spacer + render_tab_triggers + end + render_tab_contents(&) + end + end + end + end + # standard:enable Style/ArgumentsForwarding + + private + + def render_header + div do + if @title + div do + Components.Heading(level: 4) { @title.capitalize } + end + end + p { @description } if @description + end + end + + def render_tab_triggers + TabsList do + render_tab_trigger("preview", "Preview", method(:eye_icon)) + render_tab_trigger("code", "Code", method(:code_icon)) + end + end + + def render_tab_trigger(value, label, icon_method) + TabsTrigger(value: value) do + icon_method.call + span { label } + end + end + + def render_tab_contents(&) + TabsContent(value: "preview") { render_preview_tab(&) } + TabsContent(value: "code") { render_code_tab } + end + + def render_preview_tab(&block) + return iframe_preview if @src + + raw_preview + end + + def iframe_preview + div(class: "relative aspect-[4/2.5] w-full overflow-hidden rounded-md border", data: {controller: "iframe-theme"}) do + div(class: "absolute inset-0 hidden w-[1600px] bg-background md:block") do + iframe(src: @src, class: "size-full", data: {iframe_theme_target: "iframe"}) + end + end + end + + def raw_preview + div(class: "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative rounded-md border") do + div(class: "preview flex min-h-[350px] w-full justify-center p-10 items-center") do + decoded_code = CGI.unescapeHTML(@display_code) + @context.instance_eval(decoded_code) + end + end + end + + def render_code_tab + div(class: "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative rounded-md border") do + Codeblock(@display_code, syntax: :ruby, class: "-m-px") + end + end + + def eye_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + end + + def code_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" + ) + end + end + end + end +end diff --git a/docs/app/components/heading.rb b/docs/app/components/heading.rb new file mode 100644 index 00000000..f4343175 --- /dev/null +++ b/docs/app/components/heading.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module Components + class Heading < Base + def initialize(level: nil, as: nil, size: nil, **attrs) + @level = level + @as = as + @size = size + super(**attrs) + end + + def view_template(&) + tag = determine_tag + public_send(tag, **attrs, &) + end + + private + + def determine_tag + return @as if @as + return "h#{@level}" if @level + "h1" + end + + def default_attrs + { + class: class_names + } + end + + def class_names + base_classes = "scroll-m-20 font-bold tracking-tight" + size_class = size_to_class[@size || level_to_size[@level] || "6"] + tag = determine_tag + "#{base_classes} #{size_class} #{component_specific_classes(tag)}" + end + + def component_specific_classes(tag) + component_classes[tag] || "" + end + + def component_classes + { + "h1" => "scroll-m-20 text-3xl font-bold leading-normal lg:leading-normal tracking-tight lg:text-4xl", + "h2" => "scroll-m-20 text-2xl font-semibold tracking-tight transition-colors first:mt-0 pb-4 border-b", + "h4" => "scroll-m-20 text-lg font-medium tracking-tight" + } + end + + def size_to_class + { + "1" => "text-xs", + "2" => "text-sm", + "3" => "text-base", + "4" => "text-lg", + "5" => "text-xl", + "6" => "text-2xl", + "7" => "text-3xl", + "8" => "text-4xl", + "9" => "text-5xl" + } + end + + def level_to_size + { + "1" => "6", + "2" => "5", + "3" => "4", + "4" => "3" + } + end + end +end diff --git a/docs/app/components/shared/components_list.rb b/docs/app/components/shared/components_list.rb new file mode 100644 index 00000000..49c82490 --- /dev/null +++ b/docs/app/components/shared/components_list.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Components + module Shared + module ComponentsList + def components + [ + {name: "Accordion", path: docs_accordion_path}, + {name: "Alert", path: docs_alert_path}, + {name: "Alert Dialog", path: docs_alert_dialog_path}, + {name: "Aspect Ratio", path: docs_aspect_ratio_path}, + {name: "Avatar", path: docs_avatar_path}, + {name: "Badge", path: docs_badge_path}, + {name: "Breadcrumb", path: docs_breadcrumb_path}, + {name: "Button", path: docs_button_path}, + {name: "Calendar", path: docs_calendar_path}, + {name: "Card", path: docs_card_path}, + {name: "Carousel", path: docs_carousel_path}, + # { name: "Chart", path: docs_chart_path }, + {name: "Checkbox", path: docs_checkbox_path}, + {name: "Checkbox Group", path: docs_checkbox_group_path}, + {name: "Clipboard", path: docs_clipboard_path}, + {name: "Codeblock", path: docs_codeblock_path}, + {name: "Collapsible", path: docs_collapsible_path}, + {name: "Combobox", path: docs_combobox_path}, + {name: "Command", path: docs_command_path}, + {name: "Context Menu", path: docs_context_menu_path}, + {name: "Date Picker", path: docs_date_picker_path}, + {name: "Dialog / Modal", path: docs_dialog_path}, + {name: "Dropdown Menu", path: docs_dropdown_menu_path}, + {name: "Form", path: docs_form_path}, + {name: "Hover Card", path: docs_hover_card_path}, + {name: "Input", path: docs_input_path}, + {name: "Link", path: docs_link_path}, + {name: "Masked Input", path: masked_input_path}, + {name: "Pagination", path: docs_pagination_path}, + {name: "Popover", path: docs_popover_path}, + {name: "Progress", path: docs_progress_path}, + {name: "Radio Button", path: docs_radio_button_path}, + {name: "Native Select", path: docs_native_select_path}, + {name: "Select", path: docs_select_path}, + {name: "Separator", path: docs_separator_path}, + {name: "Sheet", path: docs_sheet_path}, + {name: "Shortcut Key", path: docs_shortcut_key_path}, + {name: "Sidebar", path: docs_sidebar_path}, + {name: "Skeleton", path: docs_skeleton_path}, + {name: "Switch", path: docs_switch_path}, + {name: "Table", path: docs_table_path}, + {name: "Tabs", path: docs_tabs_path}, + {name: "Textarea", path: docs_textarea_path}, + {name: "Theme Toggle", path: docs_theme_toggle_path}, + {name: "Tooltip", path: docs_tooltip_path}, + {name: "Typography", path: docs_typography_path} + ] + end + end + end +end diff --git a/docs/app/components/shared/container.rb b/docs/app/components/shared/container.rb new file mode 100644 index 00000000..4a7de82e --- /dev/null +++ b/docs/app/components/shared/container.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Components + module Shared + class Container < Components::Base + DEFAULT_CLASS = "container mx-auto w-full px-4" + SIZE_CLASSES = { + sm: "max-w-md", + md: "max-w-2xl", + lg: "max-w-4xl", + xl: "max-w-6xl", + "2xl": "max-w-7xl" + } + + def initialize(size: "md", **attrs) + @attrs = attrs + @attrs[:class] = [DEFAULT_CLASS, SIZE_CLASSES[size].to_s, @attrs[:class]] + end + + def view_template(&) + div(**@attrs, &) + end + end + end +end diff --git a/docs/app/components/shared/flash.rb b/docs/app/components/shared/flash.rb new file mode 100644 index 00000000..b1e39070 --- /dev/null +++ b/docs/app/components/shared/flash.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module Components + module Shared + class Flash < Components::Base + # STYLE_CLASS = { + # notice: 'bg-primary text-primary-foreground', + # alert: 'bg-destructive text-destructive-foreground' + # } + # TRANSITION_CLASS = "transition ease-in-out data-[state=open]:animate-in data-[state=open]:fade-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom" + + def initialize(variant: :notice, title: nil, description: nil) + @variant = variant.to_sym + @description = description + @title = title + end + + def view_template(&block) + li( + role: "status", + aria_live: "off", + aria_atomic: "true", + tabindex: "0", + data_state: "open", + data_controller: "dismissable", + # data_swipe_direction: "right", + class: [ + "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md p-4 pr-6 pt-3.5 shadow-lg transition-all data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full border", + ("bg-background text-foreground" if notice?), + ("destructive group border-destructive bg-destructive text-destructive-foreground" if alert?) + ], + style: "user-select:none; touch-action:none" + ) do + div(class: "grid gap-1") do + if @title + div(class: "text-sm font-semibold [&+div]:text-xs") { @title } + div(class: "text-sm opacity-90") { @description } + else + div { @description } + end + end + block&.call + close_button # sits at top right of toast + end + end + + private + + def close_button + button( + type: "button", + class: [ + "absolute right-1 top-1 rounded-md p-1 opacity-0 transition-opacity focus:opacity-100 focus:outline-none focus:ring-1 focus:ring-ring group-hover:opacity-100", + ("text-foreground/50 hover:text-foreground" if notice?), + ("text-destructive-foreground/50 hover:text-destructive-foreground focus:ring-destructive-foreground focus:ring-offset-destructive-foreground" if alert?) + ], + + data: { + action: "click->dismissable#dismiss" + } + ) do + x_icon + end + end + + def x_icon + svg( + width: "15", + height: "15", + viewbox: "0 0 15 15", + fill: "none", + xmlns: "http://www.w3.org/2000/svg", + class: "h-4 w-4" + ) do |s| + s.path( + d: + "M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z", + fill: "currentColor", + fill_rule: "evenodd", + clip_rule: "evenodd" + ) + end + end + + def alert? = @variant == :alert + + def notice? = @variant == :notice + end + end +end diff --git a/docs/app/components/shared/flashes.rb b/docs/app/components/shared/flashes.rb new file mode 100644 index 00000000..ad586c34 --- /dev/null +++ b/docs/app/components/shared/flashes.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Components + module Shared + class Flashes < Components::Base + def initialize(notice: nil, alert: nil) + @notice = notice + @alert = alert + end + + def view_template(&block) + ol( + tabindex: "-1", + class: + "pointer-events-none fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px] gap-y-1" + ) do + render Shared::Flash.new(variant: :notice, description: @notice) if @notice + render Shared::Flash.new(variant: :alert, description: @alert) if @alert + end + end + end + end +end diff --git a/docs/app/components/shared/footer.rb b/docs/app/components/shared/footer.rb new file mode 100644 index 00000000..13dbcc87 --- /dev/null +++ b/docs/app/components/shared/footer.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Components + module Shared + class Footer < Components::Base + def view_template + footer(class: "py-6 bg-background") do + div(class: "container flex flex-col items-center justify-center gap-4 md:h-12 md:flex-row") do + p(class: "text-balance text-center text-sm leading-loose text-foreground") do + plain "Heavily inspired by " + a( + href: "https://ui.shadcn.com", + target: "_blank", + rel: "noreferrer", + class: "font-medium underline underline-offset-4" + ) { "shadcn" } + plain ". The source code is available on " + a( + href: "https://github.com/ruby-ui/ruby_ui", + target: "_blank", + rel: "noreferrer", + class: "font-medium underline underline-offset-4" + ) { "GitHub" } + plain "." + end + end + end + end + end + end +end diff --git a/docs/app/components/shared/grid_pattern.rb b/docs/app/components/shared/grid_pattern.rb new file mode 100644 index 00000000..14166286 --- /dev/null +++ b/docs/app/components/shared/grid_pattern.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Components + module Shared + class GridPattern < Components::Base + def initialize(spacing: :md) + sizes = { + xs: 15, + sm: 25, + md: 50, + lg: 100, + xl: 200 + } + + @spacing = sizes[spacing] + end + + def view_template + svg( + class: + "absolute inset-0 -z-10 h-[calc(100vh_/_2)] w-full stroke-border [mask-image:radial-gradient(100%_100%_at_top_right,white,transparent)]", + aria_hidden: "true" + ) do |s| + s.defs do + s.pattern( + id: "0787a7c5-978c-4f66-83c7-11c213f99cb7", + width: @spacing, + height: @spacing, + x: "50%", + y: "-1", + patternunits: "userSpaceOnUse" + ) { s.path(d: "M.5 200V.5H200", fill: "none") } + end + s.rect( + width: "100%", + height: "100%", + stroke_width: "0", + fill: "url(#0787a7c5-978c-4f66-83c7-11c213f99cb7)" + ) + end + end + end + end +end diff --git a/docs/app/components/shared/head.rb b/docs/app/components/shared/head.rb new file mode 100644 index 00000000..e9a7963b --- /dev/null +++ b/docs/app/components/shared/head.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Components + module Shared + class Head < Components::Base + include Phlex::Rails::Layout + + def view_template + head do + title { "RubyUI - Component Library" } + meta name: "viewport", content: "width=device-width,initial-scale=1" + meta name: "turbo-refresh-method", content: "morph" + meta name: "turbo-refresh-scroll", content: "preserve" + meta name: "view-transition", content: "same-origin" + + link rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" + link rel: "icon", type: "image/png", sizes: "32x32", href: "/favicon-32x32.png" + link rel: "icon", type: "image/png", sizes: "16x16", href: "/favicon-16x16.png" + link rel: "manifest", href: "/site.webmanifest" + + csp_meta_tag + csrf_meta_tags + stylesheet_link_tag "https://api.fontshare.com/v2/css?f[]=general-sans@1&display=swap", data_turbo_track: "reload" + stylesheet_link_tag "application", data_turbo_track: "reload" + javascript_include_tag "application", data_turbo_track: "reload", type: "module" + end + end + end + end +end diff --git a/docs/app/components/shared/logo.rb b/docs/app/components/shared/logo.rb new file mode 100644 index 00000000..4d06f7b0 --- /dev/null +++ b/docs/app/components/shared/logo.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Components + module Shared + class Logo < Components::Base + def view_template + a(href: root_url, class: "mr-6 flex items-center space-x-2") do + Heading(level: 2, class: "flex items-center pb-0 border-0") { + img(src: image_url("logo.svg"), class: "h-4 block dark:hidden") + img(src: image_url("logo_dark.svg"), class: "h-4 hidden dark:block") + span(class: "sr-only") { "RubyUI" } + Badge(class: "ml-2 whitespace-nowrap bg-black text-white hover:bg-black/90 px-1.5 py-0.5 rounded-full text-xs font-semibold") { "1.0" } + } + end + end + end + end +end diff --git a/docs/app/components/shared/menu.rb b/docs/app/components/shared/menu.rb new file mode 100644 index 00000000..8046f4a2 --- /dev/null +++ b/docs/app/components/shared/menu.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module Components + module Shared + class Menu < Components::Base + include ComponentsList + + def view_template + div(class: "pb-4") do + # Main routes (Docs, Components, Themes, Github, Discord, Twitter) + div(class: "md:hidden") do + main_link("Docs", docs_introduction_path) + main_link("Components", docs_components_path) + main_link("Themes", theme_path("default")) + main_link("Github", "https://github.com/ruby-ui/ruby_ui") + main_link("Discord", ENV["DISCORD_INVITE_LINK"]) + main_link("Discord", ENV["DISCORD_INVITE_LINK"]) + end + + # GETTING STARTED + h4(class: "mb-1 mt-4 rounded-md px-2 py-1 text-sm font-semibold") { "Getting Started" } + div(class: "grid grid-flow-row auto-rows-max text-sm") do + getting_started_links.each do |getting_started| + menu_link(getting_started) + end + end + + # INSTALLATION + h4(class: "mb-1 mt-4 rounded-md px-2 py-1 text-sm font-semibold") { "Installation" } + div(class: "grid grid-flow-row auto-rows-max text-sm") do + installation_links.each do |installation| + menu_link(installation) + end + end + + # COMPONENTS + h4(class: "mb-1 mt-4 rounded-md px-2 py-1 text-sm font-semibold flex items-center gap-x-2") do + plain "Components" + end + div(class: "grid grid-flow-row auto-rows-max text-sm") do + components.each do |component| + menu_link(component) + end + end + end + end + + def getting_started_links + [ + {name: "Introduction", path: docs_introduction_path}, + {name: "Installation", path: docs_installation_path}, + {name: "Dark mode", path: docs_dark_mode_path}, + {name: "Theming", path: docs_theming_path}, + {name: "Customizing components", path: docs_customizing_components_path}, + {name: "Changelog", path: docs_changelog_path} + ] + end + + def installation_links + [ + {name: "Rails - JS Bundler", path: docs_installation_rails_bundler_path}, + {name: "Rails - Importmaps", path: docs_installation_rails_importmaps_path} + ] + end + + def menu_link(component) + current_path = component[:path] == request.path + a( + href: component[:path], + class: [ + "group flex w-full items-center rounded-md border border-transparent px-2 py-0.5 transition-colors", + (current_path ? "text-foreground font-semibold" : "text-foreground hover:bg-zinc-100 dark:hover:bg-zinc-800") + ] + ) do + component[:name] + end + end + + def main_link(name, path) + current_path = path == request.path + a( + href: path, + class: [ + "group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline", + (current_path ? "text-foreground font-medium" : "text-foreground/80") + ] + ) do + name + end + end + end + end +end diff --git a/docs/app/components/shared/mobile_menu.rb b/docs/app/components/shared/mobile_menu.rb new file mode 100644 index 00000000..80e98081 --- /dev/null +++ b/docs/app/components/shared/mobile_menu.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Components + module Shared + class MobileMenu < Components::Base + def initialize(**attributes) + @attributes = attributes + end + + def view_template + Sheet(class: @attributes[:class]) do + SheetTrigger do + Button(variant: :ghost, icon: true) do + menu_icon + end + end + SheetContent(class: "w-[300px]", side: :left) do + div(class: "flex flex-col h-full") do + SheetHeader do + div(class: "pl-2") do + render Shared::Logo.new + end + end + div(class: "flex-grow overflow-y-scroll") do + SheetMiddle do + render Shared::Menu.new + end + end + end + end + end + end + + def menu_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M3.75 9h16.5m-16.5 6.75h16.5" + ) + end + end + end + end +end diff --git a/docs/app/components/shared/navbar.rb b/docs/app/components/shared/navbar.rb new file mode 100644 index 00000000..c8d45178 --- /dev/null +++ b/docs/app/components/shared/navbar.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +module Components + module Shared + class Navbar < Components::Base + include ComponentsList + + def view_template + header(class: "supports-backdrop-blur:bg-background/80 sticky top-0 z-50 w-full border-b bg-background/80 backdrop-blur-2xl backdrop-saturate-200") do + div(class: "px-2 sm:px-4 sm:container flex h-14 items-center justify-between") do + div(class: "mr-4 flex items-center") do + render Shared::MobileMenu.new(class: "md:hidden") + render Shared::Logo.new + nav(class: "hidden md:flex items-center gap-6 text-sm font-medium") do + a(href: docs_introduction_path, class: "transition-colors hover:text-foreground/80 text-foreground") { "Docs" } + a(href: docs_components_path, class: "transition-colors hover:text-foreground/80 text-foreground") { "Components" } + a(href: theme_path("default"), class: "transition-colors hover:text-foreground/80 text-foreground") { "Themes" } + end + end + div(class: "flex items-center gap-x-2 md:divide-x") do + div(class: "flex items-center w-full justify-between md:justify-end gap-2") do + div(class: "w-full flex-1 md:w-auto md:flex-none") do + search_button + end + div(class: "flex items-center") do + github_link + dark_mode_toggle + end + end + end + end + end + end + + def dark_mode_toggle + ThemeToggle do + SetLightMode do + Button(variant: :ghost, icon: true) do + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + d: + "M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z" + ) + end + end + end + SetDarkMode do + Button(variant: :ghost, icon: true) do + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-4 h-4" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6a9 9 0 009 9 8.97 8.97 0 003.463-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z", + clip_rule: "evenodd" + ) + end + end + end + end + end + + def search_button + CommandDialog do + CommandDialogTrigger(keybindings: ["keydown.ctrl+k@window", "keydown.meta+k@window"]) do + Button(variant: :outline, class: "relative h-8 w-full justify-start rounded-[0.5rem] bg-muted/50 text-sm font-normal text-muted-foreground shadow-none sm:pr-12 md:w-40 lg:w-64") do + svg(xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewbox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_linejoin: "round", class: "mr-2") { |s| + s.circle(cx: "11", cy: "11", r: "8") + s.path(d: "m21 21-4.3-4.3") + } + span(class: "hidden lg:inline-flex") { "Search documentation..." } + span(class: "inline-flex lg:hidden") { "Search..." } + kbd(class: "pointer-events-none absolute right-[0.3rem] top-[0.3rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex") do + span(class: "text-xs") { "⌘" } + plain "K" + end + end + end + CommandDialogContent(class: "overflow-hidden p-0 shadow-2xl") do + Command(class: "flex h-full w-full flex-col overflow-hidden") do + CommandInput(placeholder: "Search documentation...", class: "border-none focus:ring-0") + CommandEmpty { "No results found." } + CommandList(class: "max-h-[min(450px,80vh)] overflow-y-auto p-2") do + CommandGroup(title: "Components") do + components.each do |component| + CommandItem(value: component[:name], href: component[:path], class: "px-2 py-1.5") do + plain component[:name] + end + end + end + CommandGroup(title: "Links") do + CommandItem(value: "Introduction", href: docs_introduction_path, class: "px-2 py-1.5") { "Introduction" } + CommandItem(value: "Installation", href: docs_installation_path, class: "px-2 py-1.5") { "Installation" } + CommandItem(value: "Theming", href: docs_theming_path, class: "px-2 py-1.5") { "Theming" } + end + end + end + end + end + end + + def github_link + Link(href: "https://github.com/ruby-ui/ruby_ui", variant: :ghost, icon: true) do + github_icon + end + end + + private + + def arrow_right_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 20 20", + fill: "currentColor", + class: "w-5 h-5 ml-1 -mr-1" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z", + clip_rule: "evenodd" + ) + end + end + + def chevron_down_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 20 20", + fill: "currentColor", + class: "w-4 h-4 ml-1 -mr-1" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z", + clip_rule: "evenodd" + ) + end + end + + def github_icon + svg( + viewbox: "0 0 16 16", + class: "w-4 h-4", + fill: "currentColor", + aria_hidden: "true" + ) do |s| + s.path( + d: + "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" + ) + end + end + + def account_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M18.685 19.097A9.723 9.723 0 0021.75 12c0-5.385-4.365-9.75-9.75-9.75S2.25 6.615 2.25 12a9.723 9.723 0 003.065 7.097A9.716 9.716 0 0012 21.75a9.716 9.716 0 006.685-2.653zm-12.54-1.285A7.486 7.486 0 0112 15a7.486 7.486 0 015.855 2.812A8.224 8.224 0 0112 20.25a8.224 8.224 0 01-5.855-2.438zM15.75 9a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z", + clip_rule: "evenodd" + ) + end + end + end + end +end diff --git a/docs/app/components/shared/sidebar.rb b/docs/app/components/shared/sidebar.rb new file mode 100644 index 00000000..49a5bebf --- /dev/null +++ b/docs/app/components/shared/sidebar.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Components + module Shared + class Sidebar < Components::Base + def view_template + aside(class: "fixed top-14 z-30 -ml-2 hidden h-[calc(100vh-3.5rem)] w-full shrink-0 md:sticky md:block") do + div(class: "relative overflow-hidden h-full py-6 pl-8 pr-6 lg:py-8") do + # Updated Scroll wrapper using CSS to hide scrollbar + div(class: "h-full w-full rounded-[inherit] overflow-y-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none] pb-10", data_controller: "sidebar-menu") do + render Shared::Menu.new + end + end + end + end + end + end +end diff --git a/docs/app/components/steps/builder.rb b/docs/app/components/steps/builder.rb new file mode 100644 index 00000000..d5aade1a --- /dev/null +++ b/docs/app/components/steps/builder.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Components + module Steps + class Builder < Components::Base + include DeferredRender + + def initialize(**attrs) + @attrs = attrs + @step_number = 0 + @steps = [] + end + + def view_template + div(**@attrs) do + @steps.each do |step| + render step + end + end + end + + def add_step(&) + @step_number += 1 + step = Step.new(number: @step_number, last: true, &) + @steps << step + # Last false for all steps except the last one + @steps[0..-2].each { |s| s.last = false } + end + end + end +end diff --git a/docs/app/components/steps/container.rb b/docs/app/components/steps/container.rb new file mode 100644 index 00000000..84223c60 --- /dev/null +++ b/docs/app/components/steps/container.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Components + module Steps + class Container < Components::Base + def view_template(&) + div(class: "space-y-4", &) + end + end + end +end diff --git a/docs/app/components/steps/step.rb b/docs/app/components/steps/step.rb new file mode 100644 index 00000000..50ff8347 --- /dev/null +++ b/docs/app/components/steps/step.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Components + module Steps + class Step < Components::Base + def initialize(number: 1, last: false) + @number = number + @last = last + end + + attr_writer :last + + def view_template(&block) + div(class: "relative flex space-x-4 md:space-x-8") do + div(class: "flex-shrink-0 h-full") do + div(class: "flex-0 flex items-center justify-center h-6 w-6 rounded-md border border-amber-500/20 bg-amber-100 dark:bg-amber-100/20 text-amber-700 dark:text-amber-200") do + p(class: "font-medium text-sm") { @number } + end + # vertical line unless last + hr(class: "absolute left-3 top-6 -ml-px h-full w-px bg-amber-500/20") unless @last + end + div(class: "flex-1 space-y-2 pb-10 overflow-hidden -mt-0.5", &block) + end + end + end + end +end diff --git a/docs/app/components/text.rb b/docs/app/components/text.rb new file mode 100644 index 00000000..39361c33 --- /dev/null +++ b/docs/app/components/text.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Components + class Text < Base + def initialize(as: "p", size: "3", weight: "regular", **attrs) + @as = as + @size = size + @weight = weight + super(**attrs) + end + + def view_template(&) + public_send(@as, **attrs, &) + end + + # private + + def default_attrs + { + class: class_names + } + end + + def class_names + "#{size_to_class[@size]} #{weight_to_class[@weight]} #{component_specific_classes(@as)}" + end + + def component_specific_classes(tag) + component_classes[tag] || "" + end + + def component_classes + { + "p" => "leading-7 [&:not(:first-child)]:mt-6" + } + end + + def size_to_class + { + "1" => "text-xs", "xs" => "text-xs", + "2" => "text-sm", "sm" => "text-sm", + "3" => "text-base", "base" => "text-base", + "4" => "text-lg", "lg" => "text-lg", + "5" => "text-xl", "xl" => "text-xl", + "6" => "text-2xl", "2xl" => "text-2xl", + "7" => "text-3xl", "3xl" => "text-3xl", + "8" => "text-4xl", "4xl" => "text-4xl", + "9" => "text-5xl", "5xl" => "text-5xl" + } + end + + def weight_to_class + { + "muted" => "text-muted-foreground", + "light" => "font-light", + "regular" => "font-normal", + "medium" => "font-medium", + "semibold" => "font-semibold", + "bold" => "font-bold" + } + end + end +end diff --git a/docs/app/components/themes/copy_code.rb b/docs/app/components/themes/copy_code.rb new file mode 100644 index 00000000..715faca6 --- /dev/null +++ b/docs/app/components/themes/copy_code.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Components + module Themes + class CopyCode < Components::Base + def initialize(theme:) + @theme = theme + end + + def view_template + style do + Theme::CSS.retrieve(@theme, with_directive: false) + end + Sheet do + SheetTrigger do + Button(variant: :primary) { "Copy Code" } + end + SheetContent(class: "sm:max-w-lg lg:max-w-xl flex flex-col h-screen overflow-y-scroll") do + SheetHeader do + SheetTitle { "Theme" } + SheetDescription { "Copy and paste the following code into your CSS file. These styles are compatible with TailwindCSS 4." } + end + SheetMiddle(class: "flex-1 relative") do + Codeblock(Theme::CSS.retrieve(@theme, with_directive: true, exclude_ruby_ui_vars: true), syntax: :css, class: "h-full max-h-none") + end + end + end + end + end + end +end diff --git a/docs/app/components/themes/customize_popover.rb b/docs/app/components/themes/customize_popover.rb new file mode 100644 index 00000000..a3b76eff --- /dev/null +++ b/docs/app/components/themes/customize_popover.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Components + module Themes + class CustomizePopover < Components::Base + def initialize(theme:) + @theme = theme + end + + def view_template + Popover(options: {trigger: "click", placement: "bottom-end"}) do + PopoverTrigger do + Button(variant: :outline) do + color_swatch_icon + plain "Customize Theme" + end + end + PopoverContent(class: "w-96 p-6") do + div(class: "space-y-0") do + Text(size: "4", weight: "semibold") { "Customize" } + Text(class: "text-muted-foreground") { "Choose how your app looks" } + end + div(class: "grid grid-cols-3 gap-2 mt-4") do + Theme::CSS.all_themes.each do |name, color_hash| + render_color_picker(name, color_hash, selected: name.to_s.downcase == @theme) + end + end + end + end + end + + private + + def render_color_picker(name, color_hash, selected: false) + Link(href: theme_path(name&.downcase), variant: :outline, class: ["!justify-start", ("ring-neutral-950 ring-1" if selected)]) do + div(class: "w-4 h-4 rounded-full shrink-0 mr-2 ring-white dark:hidden", style: "background-color: #{color_hash[:root][:primary].split.join(" ")}") do + end + div(class: "w-4 h-4 rounded-full shrink-0 mr-2 ring-white hidden dark:block", style: "background-color: #{color_hash[:dark][:primary].split.join(" ")}") do + end + plain name&.capitalize + end + end + + def color_swatch_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "currentColor", + viewbox: "0 0 256 256", + class: "w-5 h-5 shrink-0 mr-2 -ml-1" + ) do |s| + s.path( + d: + "M200.77,53.89A103.27,103.27,0,0,0,128,24h-1.07A104,104,0,0,0,24,128c0,43,26.58,79.06,69.36,94.17A32,32,0,0,0,136,192a16,16,0,0,1,16-16h46.21a31.81,31.81,0,0,0,31.2-24.88,104.43,104.43,0,0,0,2.59-24A103.28,103.28,0,0,0,200.77,53.89Zm13,93.71A15.89,15.89,0,0,1,198.21,160H152a32,32,0,0,0-32,32,16,16,0,0,1-21.31,15.07C62.49,194.3,40,164,40,128a88,88,0,0,1,87.09-88h.9a88.35,88.35,0,0,1,88,87.25A88.86,88.86,0,0,1,213.81,147.6ZM140,76a12,12,0,1,1-12-12A12,12,0,0,1,140,76ZM96,100A12,12,0,1,1,84,88,12,12,0,0,1,96,100Zm0,56a12,12,0,1,1-12-12A12,12,0,0,1,96,156Zm88-56a12,12,0,1,1-12-12A12,12,0,0,1,184,100Z" + ) + end + end + end + end +end diff --git a/docs/app/components/themes/grid/calendar.rb b/docs/app/components/themes/grid/calendar.rb new file mode 100644 index 00000000..ac23431e --- /dev/null +++ b/docs/app/components/themes/grid/calendar.rb @@ -0,0 +1,14 @@ +module Components + module Themes + module Grid + class Calendar < Components::Base + def view_template + div(class: "space-y-4 w-full") do + Input(type: "string", placeholder: "Select a date", class: "rounded-md border shadow", id: "formatted-date", data_controller: "input") + Calendar(input_id: "#formatted-date", date_format: "PPPP", class: "rounded-md border shadow") + end + end + end + end + end +end diff --git a/docs/app/components/themes/grid/card.rb b/docs/app/components/themes/grid/card.rb new file mode 100644 index 00000000..3c833a58 --- /dev/null +++ b/docs/app/components/themes/grid/card.rb @@ -0,0 +1,29 @@ +module Components + module Themes + module Grid + class Card < Components::Base + def view_template + RubyUI::Card(class: "w-full") do + CardHeader do + CardTitle { 'You might like "RubyUI"' } + CardDescription { "@joeldrapper" } + end + CardContent do + AspectRatio(aspect_ratio: "16/9", class: "rounded-md overflow-hidden border") do + img( + alt: "Placeholder", + loading: "lazy", + src: image_url("pattern.jpg") + ) + end + end + CardFooter(class: "flex justify-end gap-x-2") do + Button(variant: :outline) { "See more" } + Button(variant: :primary) { "Buy now" } + end + end + end + end + end + end +end diff --git a/docs/app/components/themes/grid/chart.rb b/docs/app/components/themes/grid/chart.rb new file mode 100644 index 00000000..a6dc2c2c --- /dev/null +++ b/docs/app/components/themes/grid/chart.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Components + module Themes + module Grid + class Chart < Components::Base + def view_template + RubyUI::Card(class: "p-8 space-y-6") do + div do + Text(size: "4", weight: "semibold") { "Phlex Speed Tests" } + Text(size: "2", class: "text-muted-foreground") { "Render time for a simple page" } + end + RubyUI::Chart(options: chart_options) + end + end + + private + + def chart_options + { + type: "bar", + data: { + labels: ["Phlex", "VC", "ERB"], + datasets: [{ + label: "render time (ms)", + data: [100, 520, 1200] + }] + }, + options: { + indexAxis: "y", + scales: { + y: { + beginAtZero: true + } + } + } + } + end + end + end + end +end diff --git a/docs/app/components/themes/grid/chat.rb b/docs/app/components/themes/grid/chat.rb new file mode 100644 index 00000000..0af0bac8 --- /dev/null +++ b/docs/app/components/themes/grid/chat.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module Components + module Themes + module Grid + class Chat < Components::Base + MESSAGES = [ + "You should checkout RubyUI's new release, it makes life sooo much easier", + "What's RubyUI?", + "Don't ask questions, just get on that ASAP and thank me later", + "Alright, alright, I'll check it out" + ] + + def view_template + RubyUI::Card(class: "p-8 space-y-6") do + header + messages(MESSAGES) + message_form + end + end + + private + + def header + div(class: "flex items-center justify-between") do + div(class: "flex items-center space-x-4") do + div do + Text(class: "font-medium") { "Joel Drapper" } + Text(size: "2", class: "text-muted-foreground") { "joel@drapper.me" } + end + end + Tooltip do + TooltipTrigger do + Button(variant: :outline, icon: true) do + bookmark_icon + end + end + TooltipContent do + Text { "Save contact" } + end + end + end + end + + def messages(messages) + div(class: "space-y-4") do + messages.each_with_index do |message, index| + message(message, right: index.odd?) + end + end + end + + def message(content, right: false) + div(class: ["w-full flex", ("justify-end" if right)]) do + div(class: [ + "rounded-2xl p-4 w-3/4", + (right ? "bg-primary text-primary-foreground rounded-br-sm" : "bg-muted text-foreground rounded-bl-sm") + ]) do + content + end + end + end + + def message_form + div(class: "flex w-full items-center space-x-2") do + Input(type: "message", placeholder: "Write something...") + Button { "Send" } + end + end + + def bookmark_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z" + ) + end + end + end + end + end +end diff --git a/docs/app/components/themes/grid/command.rb b/docs/app/components/themes/grid/command.rb new file mode 100644 index 00000000..d0ad5e9e --- /dev/null +++ b/docs/app/components/themes/grid/command.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +module Components + module Themes + module Grid + class Command < Components::Base + def view_template + CommandDialog do + CommandDialogTrigger do + Button(variant: "outline", class: "w-full pr-2 pl-3 justify-between") do + div(class: "flex items-center space-x-1") do + search_icon + span(class: "text-muted-foreground font-normal") do + plain "Search" + end + end + ShortcutKey do + span(class: "text-xs") { "⌘" } + plain "K" + end + end + end + CommandDialogContent do + Command do + CommandInput(placeholder: "Type a command or search...") + CommandEmpty { "No results found." } + CommandList do + CommandGroup(title: "Components") do + components_list.each do |component| + CommandItem(value: component[:name], href: component[:path]) do + default_icon + plain component[:name] + end + end + end + CommandGroup(title: "Settings") do + settings_list.each do |setting| + CommandItem(value: setting[:name], href: setting[:path]) do + default_icon + plain setting[:name] + end + end + end + end + end + end + end + end + + private + + def components + [ + ::Docs::ComponentStruct.new(name: "CommandController", source: "https://github.com/ruby-ui/ruby_ui_stimulus_pro/blob/main/controllers/command_controller.js", built_using: :stimulus), + ::Docs::ComponentStruct.new(name: "CommandDialog", source: "https://github.com/ruby-ui/ruby_ui_pro/blob/main/lib/ruby_ui_pro/command/dialog.rb", built_using: :phlex), + ::Docs::ComponentStruct.new(name: "CommandDialogTrigger", source: "https://github.com/ruby-ui/ruby_ui_pro/blob/main/lib/ruby_ui_pro/command/dialog_trigger.rb", built_using: :phlex), + ::Docs::ComponentStruct.new(name: "CommandDialogContent", source: "https://github.com/ruby-ui/ruby_ui_pro/blob/main/lib/ruby_ui_pro/command/dialog_content.rb", built_using: :phlex), + ::Docs::ComponentStruct.new(name: "Command", source: "https://github.com/ruby-ui/ruby_ui_pro/blob/main/lib/ruby_ui_pro/command.rb", built_using: :phlex), + ::Docs::ComponentStruct.new(name: "CommandInput", source: "https://github.com/ruby-ui/ruby_ui_pro/blob/main/lib/ruby_ui_pro/command/input.rb", built_using: :phlex), + ::Docs::ComponentStruct.new(name: "CommandEmpty", source: "https://github.com/ruby-ui/ruby_ui_pro/blob/main/lib/ruby_ui_pro/command/empty.rb", built_using: :phlex), + ::Docs::ComponentStruct.new(name: "CommandList", source: "https://github.com/ruby-ui/ruby_ui_pro/blob/main/lib/ruby_ui_pro/command/list.rb", built_using: :phlex), + ::Docs::ComponentStruct.new(name: "CommandGroup", source: "https://github.com/ruby-ui/ruby_ui_pro/blob/main/lib/ruby_ui_pro/command/group.rb", built_using: :phlex), + ::Docs::ComponentStruct.new(name: "CommandItem", source: "https://github.com/ruby-ui/ruby_ui_pro/blob/main/lib/ruby_ui_pro/command/item.rb", built_using: :phlex) + ] + end + + def search_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 20 20", + fill: "currentColor", + class: "w-4 h-4 mr-1.5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z", + clip_rule: "evenodd" + ) + end + end + + def default_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm4.28 10.28a.75.75 0 000-1.06l-3-3a.75.75 0 10-1.06 1.06l1.72 1.72H8.25a.75.75 0 000 1.5h5.69l-1.72 1.72a.75.75 0 101.06 1.06l3-3z", + clip_rule: "evenodd" + ) + end + end + + def components_list + [ + {name: "Accordion", path: docs_accordion_path}, + {name: "Alert", path: docs_alert_path}, + {name: "Alert Dialog", path: docs_alert_dialog_path}, + {name: "Aspect Ratio", path: docs_aspect_ratio_path}, + {name: "Avatar", path: docs_avatar_path}, + {name: "Badge", path: docs_badge_path} + ] + end + + def settings_list + [ + {name: "Profile", path: "#"}, + {name: "Mail", path: "#"}, + {name: "Settings", path: "#"} + ] + end + end + end + end +end diff --git a/docs/app/components/themes/grid/create_event.rb b/docs/app/components/themes/grid/create_event.rb new file mode 100644 index 00000000..aedab36f --- /dev/null +++ b/docs/app/components/themes/grid/create_event.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Components + module Themes + module Grid + class CreateEvent < Components::Base + def view_template + RubyUI::Card(class: "p-8 space-y-4") do + div do + Text(size: "4", weight: "semibold") { "Create an Event" } + Text(size: "2", class: "text-muted-foreground") { "Enter your event details below" } + end + event_form + end + end + + private + + def event_form + Form(class: "w-full") do + FormField do + FormFieldLabel(for: "name") { "Name" } + Input(type: "string", value: "RuSki conf. Japan", id: "name") + end + FormField do + Popover(options: {trigger: "focusin"}) do + PopoverTrigger(class: "w-full") do + div(class: "grid w-full max-w-sm items-center gap-1.5") do + FormFieldLabel(for: "date") { "Select a date" } + Input(type: "string", placeholder: "Select a date", class: "rounded-md border shadow", id: "date", data_controller: "input") + end + end + PopoverContent do + RubyUI::Calendar(input_id: "#date") + end + end + end + Button(type: "submit", class: "w-full") { "Create Event" } + end + end + end + end + end +end diff --git a/docs/app/components/themes/grid/line_graph.rb b/docs/app/components/themes/grid/line_graph.rb new file mode 100644 index 00000000..b8824b44 --- /dev/null +++ b/docs/app/components/themes/grid/line_graph.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Components + module Themes + module Grid + class LineGraph < Components::Base + def view_template + RubyUI::Card(class: "p-8 space-y-6") do + div do + Text(size: "4", weight: "semibold") { "Phlex Success" } + Text(size: "2", class: "text-muted-foreground") { "Number of stars on the Phlex Github repo" } + end + RubyUI::Chart(options: chart_options) + end + end + + private + + def chart_options + { + type: "line", + data: { + labels: ["Feb", "Mar", "Apr", "May", "Jun", "Jul"], + datasets: [{ + label: "Github Stars", + data: [40, 30, 79, 140, 290, 550] + }] + }, + options: { + scales: { + y: { + beginAtZero: true + } + }, + plugins: { + legend: { + display: false + } + } + } + } + end + end + end + end +end diff --git a/docs/app/components/themes/grid/repo_tabs.rb b/docs/app/components/themes/grid/repo_tabs.rb new file mode 100644 index 00000000..78a2b623 --- /dev/null +++ b/docs/app/components/themes/grid/repo_tabs.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +module Components + module Themes + module Grid + class RepoTabs < Components::Base + Repo = Struct.new(:github_url, :name, :stars, :version, keyword_init: true) + + def view_template + Tabs(default_value: "overview", class: "w-full") do + TabsList(class: "w-full grid grid-cols-2") do + TabsTrigger(value: "overview") do + book_icon + span(class: "ml-2") { "Overview" } + end + TabsTrigger(value: "repositories") do + repo_icon + span(class: "ml-2") { "Repositories" } + end + end + TabsContent(value: "overview") do + RubyUI::Card(class: "p-6 space-y-4 shadow-none") do + Avatar do + AvatarImage(src: "https://avatars.githubusercontent.com/u/246692?v=4", alt: "joeldrapper") + AvatarFallback { "JD" } + end + div(class: "space-y-4") do + div do + Text(size: "4", weight: "semibold") { "Joel Drapper" } + Text(size: "2", class: "text-muted-foreground") { "Creator of Phlex Components. Ruby on Rails developer." } + end + Link(href: "https://github.com/joeldrapper", variant: :outline, size: :sm) do + github_icon + span(class: "ml-2") { "View profile" } + end + end + end + end + end + end + + private + + def components + [ + ::Docs::ComponentStruct.new(name: "TabsController", source: "https://github.com/ruby-ui/ruby_ui_stimulus/blob/main/controllers/tabs_controller.js", built_using: :stimulus), + ::Docs::ComponentStruct.new(name: "Tabs", source: "https://github.com/ruby-ui/ruby_ui/blob/main/gem/lib/ruby_ui/tabs.rb", built_using: :phlex), + ::Docs::ComponentStruct.new(name: "TabsList", source: "https://github.com/ruby-ui/ruby_ui/blob/main/gem/lib/ruby_ui/tabs/list.rb", built_using: :phlex), + ::Docs::ComponentStruct.new(name: "TabsTrigger", source: "https://github.com/ruby-ui/ruby_ui/blob/main/gem/lib/ruby_ui/tabs/trigger.rb", built_using: :phlex), + ::Docs::ComponentStruct.new(name: "TabsContent", source: "https://github.com/ruby-ui/ruby_ui/blob/main/gem/lib/ruby_ui/tabs/content.rb", built_using: :phlex) + ] + end + + def book_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "2", + stroke: "currentColor", + class: "w-4 h-4 text-muted-foreground shrink-0" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" + ) + end + end + + def repo_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "2", + stroke: "currentColor", + class: "w-4 h-4 text-muted-foreground shrink-0" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" + ) + end + end + + def github_icon + svg(viewbox: "0 0 438.549 438.549", class: "h-4 w-4 shrink-0") do |s| + s.path( + fill: "currentColor", + d: + "M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z" + ) + end + end + + def star_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "2", + stroke: "currentColor", + class: "w-4 h-4 text-primary shrink-0" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" + ) + end + end + + def repositories + [ + Repo.new(github_url: "https://github.com/phlex-ruby/phlex", name: "phlex", stars: 961, version: "v1.8.1"), + Repo.new(github_url: "https://github.com/joeldrapper/green_dots", name: "green_dots", stars: 40, version: "1.0"), + Repo.new(github_url: "https://github.com/joeldrapper/literal", name: "literal", stars: 96, version: "v0.1.0") + ] + end + end + end + end +end diff --git a/docs/app/components/themes/grid/signin.rb b/docs/app/components/themes/grid/signin.rb new file mode 100644 index 00000000..8c0b3a20 --- /dev/null +++ b/docs/app/components/themes/grid/signin.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Components + module Themes + module Grid + class Signin < Components::Base + def view_template + RubyUI::Card(class: "p-8 space-y-4") do + div do + Text(size: "4", weight: "semibold") { "Create an account" } + Text(size: "2", class: "text-muted-foreground") { "Enter your email below to create your account" } + end + oauth_buttons + or_continue_with + # signin_form + end + end + + private + + def oauth_buttons + div(class: "grid grid-cols-2 gap-4 mt-4") do + # github + Button(variant: :outline, class: "w-full") do + github_icon + span(class: "ml-2") { "Github" } + end + # google + Button(variant: :outline, class: "w-full") do + google_icon + span(class: "ml-2") { "Google" } + end + end + end + + def or_continue_with + div(class: "relative") do + div(class: "absolute inset-0 flex items-center") do + span(class: "w-full border-t") + end + div(class: "relative flex justify-center text-xs uppercase") do + span(class: "bg-background px-2 text-muted-foreground") { "Or continue with" } + end + end + end + + # def signin_form + # Form::Builder.new(class: "w-full") do |f| + # f.input "email", type: :email, placeholder: "joel@drapper.me" + # f.input "password", type: :password, placeholder: "**********" + # div(class: "flex items-center space-x-3 py-2") do + # Checkbox.new(id: "terms") + # label(for: "terms", class: "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70") { "Accept terms and conditions" } + # end + # f.button(class: "w-full") { "Create account" } + # end + # end + + def github_icon + svg(viewbox: "0 0 438.549 438.549", class: "h-4 w-4") do |s| + s.path( + fill: "currentColor", + d: + "M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z" + ) + end + end + + def google_icon + svg(role: "img", viewbox: "0 0 24 24", class: "h-4 w-4") do |s| + s.path( + fill: "currentColor", + d: + "M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" + ) + end + end + end + end + end +end diff --git a/docs/app/components/themes/grid/table.rb b/docs/app/components/themes/grid/table.rb new file mode 100644 index 00000000..0f8a6189 --- /dev/null +++ b/docs/app/components/themes/grid/table.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Components + module Themes + module Grid + class Table < Components::Base + User = Struct.new(:avatar_url, :name, :username, :commits, :github_url, keyword_init: true) + + # def view_template + # render RubyUI::Card.new(class: "p-6") do + # render RubyUI::Table::Builder.new(users) do |t| + # t.column("Name") do |user| + # div(class: "flex items-center space-x-3") do + # render RubyUI::Avatar::Builder.new(src: user.avatar_url, size: :md) + # div do + # p(class: "text-sm font-medium") { user.name } + # p(class: "text-sm text-gray-500") { user.username } + # end + # end + # end + # t.column("Commits", &:commits) + # t.column("Links", header_attrs: {class: "text-right"}, footer_attrs: {class: "text-right"}) do |user| + # div(class: "flex items-center justify-end space-x-2") do + # render RubyUI::Link.new(href: github_link(user), variant: :outline, size: :sm) do + # github_icon + # span(class: "ml-2") { "See profile" } + # end + # end + # end + # end + # end + # end + + private + + def users + [ + User.new(avatar_url: "https://avatars.githubusercontent.com/u/246692?v=4", name: "Joel Drapper", username: "joeldrapper", commits: 404), + User.new(avatar_url: "https://avatars.githubusercontent.com/u/33979976?v=4", name: "Alexandre Ruban", username: "alexandreruban", commits: 16), + User.new(avatar_url: "https://avatars.githubusercontent.com/u/77887?v=4", name: "Will Cosgrove", username: "willcosgrove", commits: 12), + User.new(avatar_url: "https://avatars.githubusercontent.com/u/3025661?v=4", name: "Stephann V.", username: "stephannv", commits: 8), + User.new(avatar_url: "https://avatars.githubusercontent.com/u/6411752?v=4", name: "Marco Roth", username: "marcoroth", commits: 8) + ] + end + + def github_link(user) + "https://github.com/#{user.username}" + end + + def github_icon + svg(viewbox: "0 0 438.549 438.549", class: "h-4 w-4") do |s| + s.path( + fill: "currentColor", + d: + "M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z" + ) + end + end + end + end + end +end diff --git a/docs/app/components/typography/inline_link.rb b/docs/app/components/typography/inline_link.rb new file mode 100644 index 00000000..85b2ac95 --- /dev/null +++ b/docs/app/components/typography/inline_link.rb @@ -0,0 +1,14 @@ +module Components + module Typography + class InlineLink < Components::Base + def initialize(href:, **attrs) + @href = href + @attrs = attrs + end + + def view_template(&) + a(href: @href, **@attrs, class: "text-primary font-medium hover:underline underline-offset-4", &) + end + end + end +end diff --git a/docs/app/components/typography_list.rb b/docs/app/components/typography_list.rb new file mode 100644 index 00000000..8d6b687a --- /dev/null +++ b/docs/app/components/typography_list.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class Components::TypographyList < Components::Base + def initialize(items: [], numbered: false, **attrs) + @items = items + @numbered = numbered + super(**attrs) + end + + def view_template(&) + if @items.empty? + list(**attrs, &) + else + list(**attrs) do + @items.each do |item| + TypographyListItem { item } + end + end + end + end + + private + + def list(**attrs, &) + if numbered? + ol(**attrs, &) + else + ul(**attrs, &) + end + end + + def numbered? = @numbered + + def default_attrs + { + class: [ + "my-6 ml-6 [&>li]:mt-2 [&>li]:pl-2", + (numbered? ? "list-decimal marker:font-medium" : "list-disc") + ] + } + end +end diff --git a/docs/app/components/typography_list_item.rb b/docs/app/components/typography_list_item.rb new file mode 100644 index 00000000..7909eba9 --- /dev/null +++ b/docs/app/components/typography_list_item.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Components::TypographyListItem < Components::Base + def view_template(&) + li(**attrs, &) + end + + private + + def default_attrs + { + class: "leading-7" + } + end +end diff --git a/docs/app/controllers/application_controller.rb b/docs/app/controllers/application_controller.rb new file mode 100644 index 00000000..09705d12 --- /dev/null +++ b/docs/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/docs/app/controllers/concerns/.keep b/docs/app/controllers/concerns/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/app/controllers/docs/sidebar_controller.rb b/docs/app/controllers/docs/sidebar_controller.rb new file mode 100644 index 00000000..d6ffc469 --- /dev/null +++ b/docs/app/controllers/docs/sidebar_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Docs::SidebarController < ApplicationController + layout -> { Views::Layouts::ExamplesLayout } + + def example + sidebar_state = cookies.fetch(:sidebar_state, "true") == "true" + + render Views::Docs::Sidebar::Example.new(sidebar_state:) + end + + def inset_example + sidebar_state = cookies.fetch(:sidebar_state, "true") == "true" + + render Views::Docs::Sidebar::InsetExample.new(sidebar_state:) + end +end diff --git a/docs/app/controllers/docs_controller.rb b/docs/app/controllers/docs_controller.rb new file mode 100644 index 00000000..071964b0 --- /dev/null +++ b/docs/app/controllers/docs_controller.rb @@ -0,0 +1,228 @@ +# frozen_string_literal: true + +class DocsController < ApplicationController + layout -> { Views::Layouts::DocsLayout } + + # GETTING STARTED + def introduction + render Views::Docs::GettingStarted::Introduction.new + end + + def installation + render Views::Docs::GettingStarted::Installation.new + end + + def theming + render Views::Docs::GettingStarted::Theming.new + end + + def dark_mode + render Views::Docs::GettingStarted::DarkMode.new + end + + def customizing_components + render Views::Docs::GettingStarted::CustomizingComponents.new + end + + # INSTALLATION + def installation_rails_bundler + render Views::Docs::Installation::RailsBundler.new + end + + def installation_rails_importmaps + render Views::Docs::Installation::RailsImportmaps.new + end + + # COMPONENTS + def components + render Views::Docs::Components.new + end + + def changelog + render Views::Docs::Changelog.new + end + + def accordion + render Views::Docs::Accordion.new + end + + def alert_component # alert is a reserved word + render Views::Docs::Alert.new + end + + def alert_dialog + render Views::Docs::AlertDialog.new + end + + def aspect_ratio + render Views::Docs::AspectRatio.new + end + + def avatar + render Views::Docs::Avatar.new + end + + def badge + render Views::Docs::Badge.new + end + + def breadcrumb + render Views::Docs::Breadcrumb.new + end + + def button + render Views::Docs::Button.new + end + + def card + render Views::Docs::Card.new + end + + def carousel + render Views::Docs::Carousel.new + end + + def calendar + render Views::Docs::Calendar.new + end + + def chart + render Views::Docs::Chart.new + end + + def checkbox + render Views::Docs::Checkbox.new + end + + def checkbox_group + render Views::Docs::CheckboxGroup.new + end + + def clipboard + render Views::Docs::Clipboard.new + end + + def codeblock + render Views::Docs::Codeblock.new + end + + def collapsible + render Views::Docs::Collapsible.new + end + + def combobox + render Views::Docs::Combobox.new + end + + def command + render Views::Docs::Command.new + end + + def context_menu + render Views::Docs::ContextMenu.new + end + + def date_picker + render Views::Docs::DatePicker.new + end + + def dialog + render Views::Docs::Dialog.new + end + + def dropdown_menu + render Views::Docs::DropdownMenu.new + end + + def form + render Views::Docs::Form.new + end + + def hover_card + render Views::Docs::HoverCard.new + end + + def input + render Views::Docs::Input.new + end + + def link + render Views::Docs::Link.new + end + + def masked_input + render Views::Docs::MaskedInput.new + end + + def pagination + render Views::Docs::Pagination.new + end + + def popover + render Views::Docs::Popover.new + end + + def progress + render Views::Docs::Progress.new + end + + def radio_button + render Views::Docs::RadioButton.new + end + + def native_select + render Views::Docs::NativeSelect.new + end + + def select + render Views::Docs::Select.new + end + + def separator + render Views::Docs::Separator.new + end + + def sheet + render Views::Docs::Sheet.new + end + + def shortcut_key + render Views::Docs::ShortcutKey.new + end + + def sidebar + render Views::Docs::Sidebar.new + end + + def skeleton + render Views::Docs::Skeleton.new + end + + def switch + render Views::Docs::Switch.new + end + + def table + render Views::Docs::Table.new + end + + def tabs + render Views::Docs::Tabs.new + end + + def textarea + render Views::Docs::Textarea.new + end + + def theme_toggle + render Views::Docs::ThemeToggle.new + end + + def tooltip + render Views::Docs::Tooltip.new + end + + def typography + render Views::Docs::Typography.new + end +end diff --git a/docs/app/controllers/errors_controller.rb b/docs/app/controllers/errors_controller.rb new file mode 100644 index 00000000..9579fdbc --- /dev/null +++ b/docs/app/controllers/errors_controller.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ErrorsController < ApplicationController + layout -> { Views::Layouts::ErrorsLayout } + + def not_found + render Views::Errors::NotFound.new, status: :not_found + end + + def internal_server_error + render Views::Errors::InternalServerError.new, status: :internal_server_error + end +end diff --git a/docs/app/controllers/pages_controller.rb b/docs/app/controllers/pages_controller.rb new file mode 100644 index 00000000..6ca7a4c0 --- /dev/null +++ b/docs/app/controllers/pages_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class PagesController < ApplicationController + layout -> { Views::Layouts::PagesLayout } + + def home + render Views::Pages::Home.new + end +end diff --git a/docs/app/controllers/themes_controller.rb b/docs/app/controllers/themes_controller.rb new file mode 100644 index 00000000..1385c733 --- /dev/null +++ b/docs/app/controllers/themes_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class ThemesController < ApplicationController + layout -> { Views::Layouts::ApplicationLayout } + + # GET /themes/:theme + def show + render Views::Themes::Show.new(theme: params[:theme]) + end +end diff --git a/docs/app/helpers/application_helper.rb b/docs/app/helpers/application_helper.rb new file mode 100644 index 00000000..71014d37 --- /dev/null +++ b/docs/app/helpers/application_helper.rb @@ -0,0 +1,51 @@ +require "rubygems" + +module ApplicationHelper + def component_files(component, gem_name = "ruby_ui") + # Find the gem specification + gem_spec = Gem::Specification.find_by_name(gem_name) + return [] unless gem_spec + + # Construct the path to the component files within the gem + component_dir = File.join(gem_spec.gem_dir, "lib", "ruby_ui", camel_to_snake(component)) + + return [] unless Dir.exist?(component_dir) + + # Get all Ruby and JavaScript files + rb_files = Dir.glob(File.join(component_dir, "*.rb")) + js_files = Dir.glob(File.join(component_dir, "*_controller.js")) + + # Combine and process all files + (rb_files + js_files).map do |file| + ext = File.extname(file) + basename = File.basename(file, ext) + + name = basename.camelize + # source = "https://github.com/ruby-ui/ruby_ui/blob/v1/lib/ruby_ui/#{component.to_s.downcase}/#{File.basename(file)}" + source = "lib/ruby_ui/#{camel_to_snake(component)}/#{File.basename(file)}" + built_using = if ext == ".rb" + :phlex + else # ".js" + :stimulus + end + + ::Docs::ComponentStruct.new( + name: name, + source: source, + built_using: built_using + ) + end + end + + def camel_to_snake(string) + string.gsub("::", "/") + .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') + .gsub(/([a-z\d])([A-Z])/, '\1_\2') + .tr("-", "_") + .downcase + end + + def snake_to_camel(string) + string.split("_").map(&:capitalize).join + end +end diff --git a/docs/app/javascript/application.js b/docs/app/javascript/application.js new file mode 100644 index 00000000..6d426e62 --- /dev/null +++ b/docs/app/javascript/application.js @@ -0,0 +1,3 @@ +// Entry point for the build script in your package.json +import "@hotwired/turbo-rails"; +import "./controllers"; diff --git a/docs/app/javascript/controllers/application.js b/docs/app/javascript/controllers/application.js new file mode 100644 index 00000000..1213e85c --- /dev/null +++ b/docs/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/docs/app/javascript/controllers/iframe_theme_controller.js b/docs/app/javascript/controllers/iframe_theme_controller.js new file mode 100644 index 00000000..7b67bd78 --- /dev/null +++ b/docs/app/javascript/controllers/iframe_theme_controller.js @@ -0,0 +1,73 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["iframe"] + + connect() { + this.setupThemeObserver() + this.setupIframeListeners() + this.syncAllIframes() + } + + disconnect() { + this.observer?.disconnect() + } + + iframeTargetConnected(iframe) { + this.setupIframeListener(iframe) + this.syncThemeToIframe(iframe) + } + + setupThemeObserver() { + this.observer = new MutationObserver(() => this.syncAllIframes()) + this.observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'] + }) + } + + setupIframeListeners() { + this.iframeTargets.forEach(iframe => this.setupIframeListener(iframe)) + } + + setupIframeListener(iframe) { + if (!iframe.src) return + + iframe.addEventListener('load', () => this.syncThemeToIframe(iframe)) + + if (this.isIframeLoaded(iframe)) { + this.syncThemeToIframe(iframe) + } + } + + syncAllIframes() { + this.iframeTargets.forEach(iframe => this.syncThemeToIframe(iframe)) + } + + syncThemeToIframe(iframe) { + if (!iframe.src) return + + const iframeDoc = this.getIframeDocument(iframe) + if (!iframeDoc?.documentElement) return + + iframeDoc.documentElement.classList.toggle('dark', this.isDarkMode) + } + + getIframeDocument(iframe) { + try { + return iframe.contentDocument || iframe.contentWindow?.document + } catch (e) { + return null + } + } + + isIframeLoaded(iframe) { + const doc = this.getIframeDocument(iframe) + return doc && doc.readyState === 'complete' + } + + get isDarkMode() { + return document.documentElement.classList.contains('dark') + } +} + diff --git a/docs/app/javascript/controllers/index.js b/docs/app/javascript/controllers/index.js new file mode 100644 index 00000000..e92a12d6 --- /dev/null +++ b/docs/app/javascript/controllers/index.js @@ -0,0 +1,89 @@ +// This file is auto-generated by ./bin/rails stimulus:manifest:update +// Run that command whenever you add a new controller or create them with +// ./bin/rails generate stimulus controllerName + +import { application } from "./application" + +import IframeThemeController from "./iframe_theme_controller" +application.register("iframe-theme", IframeThemeController) + +import RubyUi__AccordionController from "./ruby_ui/accordion_controller" +application.register("ruby-ui--accordion", RubyUi__AccordionController) + +import RubyUi__AlertDialogController from "./ruby_ui/alert_dialog_controller" +application.register("ruby-ui--alert-dialog", RubyUi__AlertDialogController) + +import RubyUi__CalendarController from "./ruby_ui/calendar_controller" +application.register("ruby-ui--calendar", RubyUi__CalendarController) + +import RubyUi__CalendarInputController from "./ruby_ui/calendar_input_controller" +application.register("ruby-ui--calendar-input", RubyUi__CalendarInputController) + +import RubyUi__CarouselController from "./ruby_ui/carousel_controller" +application.register("ruby-ui--carousel", RubyUi__CarouselController) + +import RubyUi__ChartController from "./ruby_ui/chart_controller" +application.register("ruby-ui--chart", RubyUi__ChartController) + +import RubyUi__CheckboxGroupController from "./ruby_ui/checkbox_group_controller" +application.register("ruby-ui--checkbox-group", RubyUi__CheckboxGroupController) + +import RubyUi__ClipboardController from "./ruby_ui/clipboard_controller" +application.register("ruby-ui--clipboard", RubyUi__ClipboardController) + +import RubyUi__CollapsibleController from "./ruby_ui/collapsible_controller" +application.register("ruby-ui--collapsible", RubyUi__CollapsibleController) + +import RubyUi__ComboboxController from "./ruby_ui/combobox_controller" +application.register("ruby-ui--combobox", RubyUi__ComboboxController) + +import RubyUi__CommandController from "./ruby_ui/command_controller" +application.register("ruby-ui--command", RubyUi__CommandController) + +import RubyUi__ContextMenuController from "./ruby_ui/context_menu_controller" +application.register("ruby-ui--context-menu", RubyUi__ContextMenuController) + +import RubyUi__DialogController from "./ruby_ui/dialog_controller" +application.register("ruby-ui--dialog", RubyUi__DialogController) + +import RubyUi__DropdownMenuController from "./ruby_ui/dropdown_menu_controller" +application.register("ruby-ui--dropdown-menu", RubyUi__DropdownMenuController) + +import RubyUi__FormFieldController from "./ruby_ui/form_field_controller" +application.register("ruby-ui--form-field", RubyUi__FormFieldController) + +import RubyUi__HoverCardController from "./ruby_ui/hover_card_controller" +application.register("ruby-ui--hover-card", RubyUi__HoverCardController) + +import RubyUi__MaskedInputController from "./ruby_ui/masked_input_controller" +application.register("ruby-ui--masked-input", RubyUi__MaskedInputController) + +import RubyUi__PopoverController from "./ruby_ui/popover_controller" +application.register("ruby-ui--popover", RubyUi__PopoverController) + +import RubyUi__SelectController from "./ruby_ui/select_controller" +application.register("ruby-ui--select", RubyUi__SelectController) + +import RubyUi__SelectItemController from "./ruby_ui/select_item_controller" +application.register("ruby-ui--select-item", RubyUi__SelectItemController) + +import RubyUi__SheetContentController from "./ruby_ui/sheet_content_controller" +application.register("ruby-ui--sheet-content", RubyUi__SheetContentController) + +import RubyUi__SheetController from "./ruby_ui/sheet_controller" +application.register("ruby-ui--sheet", RubyUi__SheetController) + +import RubyUi__SidebarController from "./ruby_ui/sidebar_controller" +application.register("ruby-ui--sidebar", RubyUi__SidebarController) + +import RubyUi__TabsController from "./ruby_ui/tabs_controller" +application.register("ruby-ui--tabs", RubyUi__TabsController) + +import RubyUi__ThemeToggleController from "./ruby_ui/theme_toggle_controller" +application.register("ruby-ui--theme-toggle", RubyUi__ThemeToggleController) + +import RubyUi__TooltipController from "./ruby_ui/tooltip_controller" +application.register("ruby-ui--tooltip", RubyUi__TooltipController) + +import SidebarMenuController from "./sidebar_menu_controller" +application.register("sidebar-menu", SidebarMenuController) diff --git a/lib/ruby_ui/accordion/accordion_controller.js b/docs/app/javascript/controllers/ruby_ui/accordion_controller.js similarity index 100% rename from lib/ruby_ui/accordion/accordion_controller.js rename to docs/app/javascript/controllers/ruby_ui/accordion_controller.js diff --git a/lib/ruby_ui/alert_dialog/alert_dialog_controller.js b/docs/app/javascript/controllers/ruby_ui/alert_dialog_controller.js similarity index 100% rename from lib/ruby_ui/alert_dialog/alert_dialog_controller.js rename to docs/app/javascript/controllers/ruby_ui/alert_dialog_controller.js diff --git a/lib/ruby_ui/calendar/calendar_controller.js b/docs/app/javascript/controllers/ruby_ui/calendar_controller.js similarity index 100% rename from lib/ruby_ui/calendar/calendar_controller.js rename to docs/app/javascript/controllers/ruby_ui/calendar_controller.js diff --git a/lib/ruby_ui/calendar/calendar_input_controller.js b/docs/app/javascript/controllers/ruby_ui/calendar_input_controller.js similarity index 100% rename from lib/ruby_ui/calendar/calendar_input_controller.js rename to docs/app/javascript/controllers/ruby_ui/calendar_input_controller.js diff --git a/lib/ruby_ui/carousel/carousel_controller.js b/docs/app/javascript/controllers/ruby_ui/carousel_controller.js similarity index 100% rename from lib/ruby_ui/carousel/carousel_controller.js rename to docs/app/javascript/controllers/ruby_ui/carousel_controller.js diff --git a/lib/ruby_ui/chart/chart_controller.js b/docs/app/javascript/controllers/ruby_ui/chart_controller.js similarity index 100% rename from lib/ruby_ui/chart/chart_controller.js rename to docs/app/javascript/controllers/ruby_ui/chart_controller.js diff --git a/lib/ruby_ui/checkbox/checkbox_group_controller.js b/docs/app/javascript/controllers/ruby_ui/checkbox_group_controller.js similarity index 100% rename from lib/ruby_ui/checkbox/checkbox_group_controller.js rename to docs/app/javascript/controllers/ruby_ui/checkbox_group_controller.js diff --git a/lib/ruby_ui/clipboard/clipboard_controller.js b/docs/app/javascript/controllers/ruby_ui/clipboard_controller.js similarity index 100% rename from lib/ruby_ui/clipboard/clipboard_controller.js rename to docs/app/javascript/controllers/ruby_ui/clipboard_controller.js diff --git a/lib/ruby_ui/collapsible/collapsible_controller.js b/docs/app/javascript/controllers/ruby_ui/collapsible_controller.js similarity index 100% rename from lib/ruby_ui/collapsible/collapsible_controller.js rename to docs/app/javascript/controllers/ruby_ui/collapsible_controller.js diff --git a/docs/app/javascript/controllers/ruby_ui/combobox_controller.js b/docs/app/javascript/controllers/ruby_ui/combobox_controller.js new file mode 100644 index 00000000..d1932772 --- /dev/null +++ b/docs/app/javascript/controllers/ruby_ui/combobox_controller.js @@ -0,0 +1,191 @@ +import { Controller } from "@hotwired/stimulus"; +import { computePosition, autoUpdate, offset, flip } from "@floating-ui/dom"; + +// Connects to data-controller="ruby-ui--combobox" +export default class extends Controller { + static values = { + term: String + } + + static targets = [ + "input", + "toggleAll", + "popover", + "item", + "emptyState", + "searchInput", + "trigger", + "triggerContent" + ] + + selectedItemIndex = null + + connect() { + this.updateTriggerContent() + } + + disconnect() { + if (this.cleanup) { this.cleanup() } + } + + handlePopoverToggle(event) { + // Keep ariaExpanded in sync with the actual popover state + this.triggerTarget.ariaExpanded = event.newState === 'open' ? 'true' : 'false' + } + + inputChanged(e) { + this.updateTriggerContent() + + if (e.target.type == "radio") { + this.closePopover() + } + + if (this.hasToggleAllTarget && !e.target.checked) { + this.toggleAllTarget.checked = false + } + } + + inputContent(input) { + return input.dataset.text || input.parentElement.textContent + } + + toggleAllItems() { + const isChecked = this.toggleAllTarget.checked + this.inputTargets.forEach(input => input.checked = isChecked) + this.updateTriggerContent() + } + + updateTriggerContent() { + const checkedInputs = this.inputTargets.filter(input => input.checked) + + if (checkedInputs.length === 0) { + this.triggerContentTarget.innerText = this.triggerTarget.dataset.placeholder + } else if (this.termValue && checkedInputs.length > 1) { + this.triggerContentTarget.innerText = `${checkedInputs.length} ${this.termValue}` + } else { + this.triggerContentTarget.innerText = checkedInputs.map((input) => this.inputContent(input)).join(", ") + } + } + + togglePopover(event) { + event.preventDefault() + + if (this.triggerTarget.ariaExpanded === "true") { + this.closePopover() + } else { + this.openPopover(event) + } + } + + openPopover(event) { + if (event) event.preventDefault() + + this.updatePopoverPosition() + this.updatePopoverWidth() + this.triggerTarget.ariaExpanded = "true" + this.selectedItemIndex = null + this.itemTargets.forEach(item => item.ariaCurrent = "false") + this.popoverTarget.showPopover() + } + + closePopover() { + this.triggerTarget.ariaExpanded = "false" + this.popoverTarget.hidePopover() + } + + filterItems(e) { + if (["ArrowDown", "ArrowUp", "Tab", "Enter"].includes(e.key)) { + return + } + + const filterTerm = this.searchInputTarget.value.toLowerCase() + + if (this.hasToggleAllTarget) { + if (filterTerm) this.toggleAllTarget.parentElement.classList.add("hidden") + else this.toggleAllTarget.parentElement.classList.remove("hidden") + } + + let resultCount = 0 + + this.selectedItemIndex = null + + this.inputTargets.forEach((input) => { + const text = this.inputContent(input).toLowerCase() + + if (text.indexOf(filterTerm) > -1) { + input.parentElement.classList.remove("hidden") + resultCount++ + } else { + input.parentElement.classList.add("hidden") + } + }) + + this.emptyStateTarget.classList.toggle("hidden", resultCount !== 0) + } + + keyDownPressed() { + if (this.selectedItemIndex !== null) { + this.selectedItemIndex++ + } else { + this.selectedItemIndex = 0 + } + + this.focusSelectedInput() + } + + keyUpPressed() { + if (this.selectedItemIndex !== null) { + this.selectedItemIndex-- + } else { + this.selectedItemIndex = -1 + } + + this.focusSelectedInput() + } + + focusSelectedInput() { + const visibleInputs = this.inputTargets.filter(input => !input.parentElement.classList.contains("hidden")) + + this.wrapSelectedInputIndex(visibleInputs.length) + + visibleInputs.forEach((input, index) => { + if (index == this.selectedItemIndex) { + input.parentElement.ariaCurrent = "true" + input.parentElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }) + } else { + input.parentElement.ariaCurrent = "false" + } + }) + } + + keyEnterPressed(event) { + event.preventDefault() + const option = this.itemTargets.find(item => item.ariaCurrent === "true") + + if (option) { + option.click() + } + } + + wrapSelectedInputIndex(length) { + this.selectedItemIndex = ((this.selectedItemIndex % length) + length) % length + } + + updatePopoverPosition() { + this.cleanup = autoUpdate(this.triggerTarget, this.popoverTarget, () => { + computePosition(this.triggerTarget, this.popoverTarget, { + placement: 'bottom-start', + middleware: [offset(4), flip()], + }).then(({ x, y }) => { + Object.assign(this.popoverTarget.style, { + left: `${x}px`, + top: `${y}px`, + }); + }); + }); + } + + updatePopoverWidth() { + this.popoverTarget.style.width = `${this.triggerTarget.offsetWidth}px` + } +} diff --git a/lib/ruby_ui/command/command_controller.js b/docs/app/javascript/controllers/ruby_ui/command_controller.js similarity index 100% rename from lib/ruby_ui/command/command_controller.js rename to docs/app/javascript/controllers/ruby_ui/command_controller.js diff --git a/lib/ruby_ui/context_menu/context_menu_controller.js b/docs/app/javascript/controllers/ruby_ui/context_menu_controller.js similarity index 100% rename from lib/ruby_ui/context_menu/context_menu_controller.js rename to docs/app/javascript/controllers/ruby_ui/context_menu_controller.js diff --git a/lib/ruby_ui/dialog/dialog_controller.js b/docs/app/javascript/controllers/ruby_ui/dialog_controller.js similarity index 100% rename from lib/ruby_ui/dialog/dialog_controller.js rename to docs/app/javascript/controllers/ruby_ui/dialog_controller.js diff --git a/lib/ruby_ui/dropdown_menu/dropdown_menu_controller.js b/docs/app/javascript/controllers/ruby_ui/dropdown_menu_controller.js similarity index 100% rename from lib/ruby_ui/dropdown_menu/dropdown_menu_controller.js rename to docs/app/javascript/controllers/ruby_ui/dropdown_menu_controller.js diff --git a/lib/ruby_ui/form/form_field_controller.js b/docs/app/javascript/controllers/ruby_ui/form_field_controller.js similarity index 100% rename from lib/ruby_ui/form/form_field_controller.js rename to docs/app/javascript/controllers/ruby_ui/form_field_controller.js diff --git a/lib/ruby_ui/hover_card/hover_card_controller.js b/docs/app/javascript/controllers/ruby_ui/hover_card_controller.js similarity index 100% rename from lib/ruby_ui/hover_card/hover_card_controller.js rename to docs/app/javascript/controllers/ruby_ui/hover_card_controller.js diff --git a/docs/app/javascript/controllers/ruby_ui/masked_input_controller.js b/docs/app/javascript/controllers/ruby_ui/masked_input_controller.js new file mode 100644 index 00000000..dfea0945 --- /dev/null +++ b/docs/app/javascript/controllers/ruby_ui/masked_input_controller.js @@ -0,0 +1,9 @@ +import { Controller } from "@hotwired/stimulus"; +import { MaskInput } from "maska"; + +// Connects to data-controller="ruby-ui--masked-input" +export default class extends Controller { + connect() { + new MaskInput(this.element) + } +} diff --git a/lib/ruby_ui/popover/popover_controller.js b/docs/app/javascript/controllers/ruby_ui/popover_controller.js similarity index 100% rename from lib/ruby_ui/popover/popover_controller.js rename to docs/app/javascript/controllers/ruby_ui/popover_controller.js diff --git a/lib/ruby_ui/select/select_controller.js b/docs/app/javascript/controllers/ruby_ui/select_controller.js similarity index 100% rename from lib/ruby_ui/select/select_controller.js rename to docs/app/javascript/controllers/ruby_ui/select_controller.js diff --git a/lib/ruby_ui/select/select_item_controller.js b/docs/app/javascript/controllers/ruby_ui/select_item_controller.js similarity index 100% rename from lib/ruby_ui/select/select_item_controller.js rename to docs/app/javascript/controllers/ruby_ui/select_item_controller.js diff --git a/lib/ruby_ui/sheet/sheet_content_controller.js b/docs/app/javascript/controllers/ruby_ui/sheet_content_controller.js similarity index 100% rename from lib/ruby_ui/sheet/sheet_content_controller.js rename to docs/app/javascript/controllers/ruby_ui/sheet_content_controller.js diff --git a/lib/ruby_ui/sheet/sheet_controller.js b/docs/app/javascript/controllers/ruby_ui/sheet_controller.js similarity index 100% rename from lib/ruby_ui/sheet/sheet_controller.js rename to docs/app/javascript/controllers/ruby_ui/sheet_controller.js diff --git a/lib/ruby_ui/sidebar/sidebar_controller.js b/docs/app/javascript/controllers/ruby_ui/sidebar_controller.js similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_controller.js rename to docs/app/javascript/controllers/ruby_ui/sidebar_controller.js diff --git a/lib/ruby_ui/tabs/tabs_controller.js b/docs/app/javascript/controllers/ruby_ui/tabs_controller.js similarity index 100% rename from lib/ruby_ui/tabs/tabs_controller.js rename to docs/app/javascript/controllers/ruby_ui/tabs_controller.js diff --git a/lib/ruby_ui/theme_toggle/theme_toggle_controller.js b/docs/app/javascript/controllers/ruby_ui/theme_toggle_controller.js similarity index 100% rename from lib/ruby_ui/theme_toggle/theme_toggle_controller.js rename to docs/app/javascript/controllers/ruby_ui/theme_toggle_controller.js diff --git a/lib/ruby_ui/tooltip/tooltip_controller.js b/docs/app/javascript/controllers/ruby_ui/tooltip_controller.js similarity index 100% rename from lib/ruby_ui/tooltip/tooltip_controller.js rename to docs/app/javascript/controllers/ruby_ui/tooltip_controller.js diff --git a/docs/app/javascript/controllers/sidebar_menu_controller.js b/docs/app/javascript/controllers/sidebar_menu_controller.js new file mode 100644 index 00000000..376c061e --- /dev/null +++ b/docs/app/javascript/controllers/sidebar_menu_controller.js @@ -0,0 +1,17 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="sidebar-menu" +export default class extends Controller { + connect() { + window.addEventListener("turbo:before-cache", () => { + localStorage.setItem("menuScrollPositon", this.element.scrollTop); + }); + + window.addEventListener("turbo:before-render", () => { + this.element.scrollTop = localStorage.getItem("menuScrollPositon") || 0; + }); + window.addEventListener("turbo:render", () => { + this.element.scrollTop = localStorage.getItem("menuScrollPositon") || 0; + }); + } +} diff --git a/docs/app/jobs/application_job.rb b/docs/app/jobs/application_job.rb new file mode 100644 index 00000000..d394c3d1 --- /dev/null +++ b/docs/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/docs/app/lib/deferred_render.rb b/docs/app/lib/deferred_render.rb new file mode 100644 index 00000000..b946de72 --- /dev/null +++ b/docs/app/lib/deferred_render.rb @@ -0,0 +1,6 @@ +module DeferredRender + def before_template(&) + vanish(&) + super + end +end diff --git a/docs/app/lib/ruby_ui/file_manager.rb b/docs/app/lib/ruby_ui/file_manager.rb new file mode 100644 index 00000000..b1b3d58b --- /dev/null +++ b/docs/app/lib/ruby_ui/file_manager.rb @@ -0,0 +1,36 @@ +module RubyUI + module FileManager + extend self + + def main_component_code(component_name) + component_code main_component_file_path(component_name) + end + + def component_code(file_path) + File.read(file_path) if File.exist?(file_path) + end + + def component_file_paths(component_name) + Dir[File.join(component_folder(component_name), "*.rb")] + end + + def stimulus_controller_file_paths(component_name) + Dir[File.join(component_folder(component_name), "*.js")] + end + + def component_folder(component_name) + component_name = component_name.underscore + File.join(gem_path, "lib", "ruby_ui", component_name) + end + + def dependencies(component_name) + DEPENDENCIES[component_name.underscore].to_h + end + + def gem_path + @gem_path ||= Gem::Specification.find_by_name("ruby_ui").gem_dir + end + + DEPENDENCIES = YAML.load_file(File.join(gem_path, "lib/generators/ruby_ui/dependencies.yml")).freeze + end +end diff --git a/docs/app/mailers/application_mailer.rb b/docs/app/mailers/application_mailer.rb new file mode 100644 index 00000000..7809ebdf --- /dev/null +++ b/docs/app/mailers/application_mailer.rb @@ -0,0 +1,9 @@ +class ApplicationMailer < ActionMailer::Base + default from: email_address_with_name(ENV["MAILER_SENDER"], ENV["APP_NAME"]), + reply_to: ENV["MAILER_SENDER"] + layout -> { Views::Layouts::MailerLayout } + + def self.template_path + "mailers/#{name.underscore}" + end +end diff --git a/docs/app/mailers/signin_link_mailer.rb b/docs/app/mailers/signin_link_mailer.rb new file mode 100644 index 00000000..2fef227e --- /dev/null +++ b/docs/app/mailers/signin_link_mailer.rb @@ -0,0 +1,14 @@ +class SigninLinkMailer < ApplicationMailer + def signin_link + @user = params[:user] + @token = params[:token] + @redirect_path = params[:redirect_path] + + mail( + to: @user.email, + subject: "Your Magic Sign-in Link" + ) do |format| + format.html { render Mailers::SigninLinkMailer::SigninLink.new(user: @user, token: @token, redirect_path: @redirect_path) } + end + end +end diff --git a/docs/app/mailers/team_member_mailer.rb b/docs/app/mailers/team_member_mailer.rb new file mode 100644 index 00000000..dc557f42 --- /dev/null +++ b/docs/app/mailers/team_member_mailer.rb @@ -0,0 +1,13 @@ +class TeamMemberMailer < ApplicationMailer + def invite + @user = params[:user] + @email = params[:email] + + mail( + to: @email, + subject: "You've been invited to join #{ENV["APP_NAME"]}!" + ) do |format| + format.html { render Mailers::TeamMemberMailer::Invite.new(user: @user, email: @email) } + end + end +end diff --git a/docs/app/mailers/user_mailer.rb b/docs/app/mailers/user_mailer.rb new file mode 100644 index 00000000..cf1ea000 --- /dev/null +++ b/docs/app/mailers/user_mailer.rb @@ -0,0 +1,12 @@ +class UserMailer < ApplicationMailer + def welcome + @user = params[:user] + + mail( + to: @user.email, + subject: "Welcome to RubyUI" + ) do |format| + format.html { render Mailers::UserMailer::Welcome.new(user: @user) } + end + end +end diff --git a/docs/app/models/application_record.rb b/docs/app/models/application_record.rb new file mode 100644 index 00000000..b63caeb8 --- /dev/null +++ b/docs/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/docs/app/models/concerns/.keep b/docs/app/models/concerns/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/app/models/post.rb b/docs/app/models/post.rb new file mode 100644 index 00000000..c7ecd23d --- /dev/null +++ b/docs/app/models/post.rb @@ -0,0 +1 @@ +Post = Struct.new(:id, :person_id) diff --git a/docs/app/views/base.rb b/docs/app/views/base.rb new file mode 100644 index 00000000..ff3b1062 --- /dev/null +++ b/docs/app/views/base.rb @@ -0,0 +1,11 @@ +class Views::Base < Components::Base + include ApplicationHelper + + GITHUB_REPO_URL = "https://github.com/ruby-ui/ruby_ui/" + GITHUB_FILE_URL = "#{GITHUB_REPO_URL}blob/main/" + + def before_template + Docs::VisualCodeExample.reset_collected_code + super + end +end diff --git a/docs/app/views/docs/accordion.rb b/docs/app/views/docs/accordion.rb new file mode 100644 index 00000000..f899a3d7 --- /dev/null +++ b/docs/app/views/docs/accordion.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class Views::Docs::Accordion < Views::Base + def view_template + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + component = "Accordion" + render Docs::Header.new(title: component, + description: "A vertically stacked set of interactive headings that each reveal a section of content.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + @@code = <<~RUBY + div(class: "w-full") do + Accordion do + AccordionItem do + AccordionTrigger do + p(class: "font-medium") { "What is RubyUI?" } + AccordionIcon() + end + + AccordionContent do + p(class: "text-sm pb-4") do + "RubyUI is a UI component library for Ruby devs who want to build better, faster." + end + end + end + end + + Accordion do + AccordionItem do + AccordionTrigger do + p(class: "font-medium") { "Can I use it with Rails?" } + AccordionIcon() + end + + AccordionContent do + p(class: "text-sm pb-4") do + "Yes, RubyUI is pure Ruby and works great with Rails. It's a Ruby gem that you can install into your Rails app." + end + end + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/alert/alert_docs.rb b/docs/app/views/docs/alert.rb similarity index 100% rename from lib/ruby_ui/alert/alert_docs.rb rename to docs/app/views/docs/alert.rb diff --git a/lib/ruby_ui/alert_dialog/alert_dialog_docs.rb b/docs/app/views/docs/alert_dialog.rb similarity index 100% rename from lib/ruby_ui/alert_dialog/alert_dialog_docs.rb rename to docs/app/views/docs/alert_dialog.rb diff --git a/lib/ruby_ui/aspect_ratio/aspect_ratio_docs.rb b/docs/app/views/docs/aspect_ratio.rb similarity index 100% rename from lib/ruby_ui/aspect_ratio/aspect_ratio_docs.rb rename to docs/app/views/docs/aspect_ratio.rb diff --git a/lib/ruby_ui/avatar/avatar_docs.rb b/docs/app/views/docs/avatar.rb similarity index 100% rename from lib/ruby_ui/avatar/avatar_docs.rb rename to docs/app/views/docs/avatar.rb diff --git a/lib/ruby_ui/badge/badge_docs.rb b/docs/app/views/docs/badge.rb similarity index 100% rename from lib/ruby_ui/badge/badge_docs.rb rename to docs/app/views/docs/badge.rb diff --git a/lib/ruby_ui/breadcrumb/breadcrumb_docs.rb b/docs/app/views/docs/breadcrumb.rb similarity index 100% rename from lib/ruby_ui/breadcrumb/breadcrumb_docs.rb rename to docs/app/views/docs/breadcrumb.rb diff --git a/lib/ruby_ui/button/button_docs.rb b/docs/app/views/docs/button.rb similarity index 100% rename from lib/ruby_ui/button/button_docs.rb rename to docs/app/views/docs/button.rb diff --git a/lib/ruby_ui/calendar/calendar_docs.rb b/docs/app/views/docs/calendar.rb similarity index 100% rename from lib/ruby_ui/calendar/calendar_docs.rb rename to docs/app/views/docs/calendar.rb diff --git a/lib/ruby_ui/card/card_docs.rb b/docs/app/views/docs/card.rb similarity index 100% rename from lib/ruby_ui/card/card_docs.rb rename to docs/app/views/docs/card.rb diff --git a/lib/ruby_ui/carousel/carousel_docs.rb b/docs/app/views/docs/carousel.rb similarity index 100% rename from lib/ruby_ui/carousel/carousel_docs.rb rename to docs/app/views/docs/carousel.rb diff --git a/docs/app/views/docs/changelog.rb b/docs/app/views/docs/changelog.rb new file mode 100644 index 00000000..18066a73 --- /dev/null +++ b/docs/app/views/docs/changelog.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "rss" +require "net/http" + +class Views::Docs::Changelog < Views::Base + def view_template + div(class: "mx-auto max-w-[800px] py-10") do + h1(class: "scroll-m-24 text-3xl font-semibold tracking-tight sm:text-3xl mb-4") { "Changelog" } + p(class: "text-xl text-foreground mb-8") { "Latest updates and announcements from the RubyUI team." } + + if releases.any? + div(class: "space-y-12") do + releases.each do |release| + div(class: "flex flex-col items-start gap-4 md:flex-row md:items-baseline md:gap-8") do + div(class: "w-full md:w-32 shrink-0") do + p(class: "text-sm text-foreground/70") { format_date(release[:published_at]) } + end + div(class: "grid gap-4 w-full") do + h2(class: "text-2xl font-bold tracking-tight inline-flex items-center gap-2") do + plain release[:name] + end + div(class: "prose prose-slate dark:prose-invert max-w-none [&>ul]:my-6 [&>ul]:ml-6 [&>ul]:list-disc [&>ul>li]:mt-2 [&>h3]:text-xl [&>h3]:font-bold [&>h2]:text-2xl [&>h2]:font-bold") do + raw(release[:body].to_s.html_safe) + end + end + end + Separator(class: "my-8") + end + end + else + p(class: "text-muted-foreground") { "No releases found." } + end + end + end + + private + + def releases + @releases ||= begin + url = "https://github.com/ruby-ui/ruby_ui/releases.atom" + feed = RSS::Parser.parse(Net::HTTP.get(URI.parse(url)), false) + feed.items.map do |item| + { + name: item.title.content, + published_at: item.updated.content.to_s, + body: item.content.content + } + end + rescue + [] + end + end + + def format_date(date_string) + datetime = DateTime.parse(date_string) + datetime.strftime("%B %d, %Y") + rescue + date_string + end +end diff --git a/docs/app/views/docs/chart.rb b/docs/app/views/docs/chart.rb new file mode 100644 index 00000000..62d032e6 --- /dev/null +++ b/docs/app/views/docs/chart.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +class Views::Docs::Chart < Views::Base + def view_template + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Chart", description: "Displays information in a visual way.") + + Heading(level: 2) { "Introduction" } + + Text do + plain "RubyUI uses " + InlineLink(href: "https://www.chartjs.org/") { "Chart.js" } + plain " to render charts. Chart.js is a free open-source JavaScript library for data visualization, which supports 8 chart types: bar, line, area, pie, bubble, radar, polar, and scatter. If you're unfamiliar with Chart.js. We recommend the " + InlineLink(href: "https://www.chartjs.org/docs/latest/getting-started/") { "Getting Started guide" } + plain ". " + end + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Bar Chart", context: self) do + <<~RUBY + options = { + type: 'bar', + data: { + labels: ['Phlex', 'VC', 'ERB'], + datasets: [{ + label: 'render time (ms)', + data: [100, 520, 1200], + }] + }, + options: { + indexAxis: 'y', + scales: { + y: { + beginAtZero: true + } + }, + }, + } + + Chart(options: options) + RUBY + end + + render Docs::VisualCodeExample.new(title: "Line Graph", context: self) do + <<~RUBY + options = { + type: 'line', + data: { + labels: ['Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'], + datasets: [{ + label: 'Github Stars', + data: [40, 30, 79, 140, 290, 550], + }] + }, + options: { + scales: { + y: { + beginAtZero: true + } + }, + plugins: { + legend: { + display: false + } + } + }, + } + + Chart(options: options) + RUBY + end + + render Docs::VisualCodeExample.new(title: "Pie Chart", description: "Setting custom background color", context: self) do + <<~RUBY + options = { + type: 'pie', + data: { + labels: [ + 'Red', + 'Blue', + 'Yellow' + ], + datasets: [{ + label: 'My First Dataset', + data: [300, 50, 100], + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 205, 86)' + ], + hoverOffset: 4 + }] + }, + } + + Chart(options: options) + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: "Chart") + + render Docs::ComponentsTable.new(components) + end + end + + private + + def components + [ + ::Docs::ComponentStruct.new(name: "ChartController", source: "https://github.com/ruby-ui/ruby_ui_stimulus/blob/main/controllers/chart_controller.js", built_using: :stimulus), + ::Docs::ComponentStruct.new(name: "Chart", source: "https://github.com/ruby-ui/ruby_ui/blob/main/gem/lib/ruby_ui/chart.rb", built_using: :phlex) + ] + end +end diff --git a/lib/ruby_ui/checkbox/checkbox_docs.rb b/docs/app/views/docs/checkbox.rb similarity index 100% rename from lib/ruby_ui/checkbox/checkbox_docs.rb rename to docs/app/views/docs/checkbox.rb diff --git a/docs/app/views/docs/checkbox_group.rb b/docs/app/views/docs/checkbox_group.rb new file mode 100644 index 00000000..73c5b05c --- /dev/null +++ b/docs/app/views/docs/checkbox_group.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +class Views::Docs::CheckboxGroup < Views::Base + def view_template + component = "Checkbox" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Checkbox Group", description: "A control that allows the user to toggle between checked and not checked.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + CheckboxGroup(data_required: true) do + div(class: "flex flex-col gap-2") do + div(class: "flex flex-row items-center gap-2") do + Checkbox(value: "FOO", id: "EXAMPLE_FOO") + FormFieldLabel(for: "EXAMPLE_FOO") { "FOO" } + end + + div(class: "flex flex-row items-center gap-2") do + Checkbox(value: "BAR", id: "EXAMPLE_BAR") + FormFieldLabel(for: "EXAMPLE_BAR") { "BAR" } + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "With Form", context: self) do + <<~RUBY + form(class: "flex flex-col gap-2") do + FormField do + FormFieldLabel { "CHECKBOX_GROUP" } + + FormFieldHint { "HINT_FOR_CHECKBOX_GROUP" } + + CheckboxGroup(data_required: true) do + div(class: "flex flex-col gap-2") do + div(class: "flex flex-row items-center gap-2") do + Checkbox( + id: "FORM_FOO", + value: "FOO", + checked: false, + name: "CHECKBOX_GROUP[]", + data: {value_missing: "CUSTOM_MESSAGE"} + ) + + FormFieldLabel(for: "FORM_FOO") { "FOO" } + end + + div(class: "flex flex-row items-center gap-2") do + Checkbox( + id: "FORM_BAR", + value: "BAR", + checked: true, + name: "CHECKBOX_GROUP[]", + data: {value_missing: "CUSTOM_MESSAGE"} + ) + + FormFieldLabel(for: "FORM_BAR") { "BAR" } + end + end + end + + FormFieldError() + end + + Button(type: "submit") { "SUBMIT_BUTTON" } + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/clipboard/clipboard_docs.rb b/docs/app/views/docs/clipboard.rb similarity index 100% rename from lib/ruby_ui/clipboard/clipboard_docs.rb rename to docs/app/views/docs/clipboard.rb diff --git a/lib/ruby_ui/codeblock/codeblock_docs.rb b/docs/app/views/docs/codeblock.rb similarity index 100% rename from lib/ruby_ui/codeblock/codeblock_docs.rb rename to docs/app/views/docs/codeblock.rb diff --git a/lib/ruby_ui/collapsible/collapsible_docs.rb b/docs/app/views/docs/collapsible.rb similarity index 100% rename from lib/ruby_ui/collapsible/collapsible_docs.rb rename to docs/app/views/docs/collapsible.rb diff --git a/docs/app/views/docs/combobox.rb b/docs/app/views/docs/combobox.rb new file mode 100644 index 00000000..2fd1d422 --- /dev/null +++ b/docs/app/views/docs/combobox.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +class Views::Docs::Combobox < Views::Base + @@code_example = nil + + def view_template + component = "Combobox" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: component, description: "Autocomplete input and command palette with a list of suggestions.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Single option", context: self) do + <<~RUBY + div class: "w-96" do + Combobox do + ComboboxTrigger placeholder: "Pick value" + + ComboboxPopover do + ComboboxSearchInput(placeholder: "Pick value or type anything") + + ComboboxList do + ComboboxEmptyState { "No result" } + + ComboboxListGroup(label: "Fruits") do + ComboboxItem do + ComboboxRadio(name: "food", value: "apple") + span { "Apple" } + end + + ComboboxItem do + ComboboxRadio(name: "food", value: "banana") + span { "Banana" } + end + end + + ComboboxListGroup(label: "Vegetable") do + ComboboxItem do + ComboboxRadio(name: "food", value: "brocoli") + span { "Broccoli" } + end + + ComboboxItem do + ComboboxRadio(name: "food", value: "carrot") + span { "Carrot" } + end + end + + ComboboxListGroup(label: "Others") do + ComboboxItem do + ComboboxRadio(name: "food", value: "chocolate") + span { "Chocolate" } + end + + ComboboxItem do + ComboboxRadio(name: "food", value: "milk") + span { "Milk" } + end + end + end + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Multiple options", context: self) do + <<~RUBY + div class: "w-96" do + Combobox term: "things" do + ComboboxTrigger placeholder: "Pick value" + + ComboboxPopover do + ComboboxSearchInput(placeholder: "Pick value or type anything") + + ComboboxList do + ComboboxEmptyState { "No result" } + + ComboboxItem(class: "mt-3") do + ComboboxToggleAllCheckbox(name: "all", value: "all") + span { "Select all" } + end + + ComboboxListGroup label: "Fruits" do + ComboboxItem do + ComboboxCheckbox(name: "food", value: "apple") + span { "Apple" } + end + + ComboboxItem do + ComboboxCheckbox(name: "food", value: "banana") + span { "Banana" } + end + end + + ComboboxListGroup label: "Vegetable" do + ComboboxItem do + ComboboxCheckbox(name: "food", value: "brocoli") + span { "Broccoli" } + end + + ComboboxItem do + ComboboxCheckbox(name: "food", value: "carrot") + span { "Carrot" } + end + end + + ComboboxListGroup label: "Others" do + ComboboxItem do + ComboboxCheckbox(name: "food", value: "chocolate") + span { "Chocolate" } + end + + ComboboxItem do + ComboboxCheckbox(name: "food", value: "milk") + span { "Milk" } + end + end + end + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Disabled", context: self) do + <<~RUBY + div(class: "w-96") do + Combobox do + ComboboxTrigger(disabled: true, placeholder: "Pick value") + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Aria Disabled", context: self) do + <<~RUBY + div(class: "w-96") do + Combobox do + ComboboxTrigger(aria: {disabled: "true"}, placeholder: "Pick value") + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: "Combobox") + + render Docs::ComponentsTable.new(component_files("Combobox")) + end + end +end diff --git a/lib/ruby_ui/command/command_docs.rb b/docs/app/views/docs/command.rb similarity index 100% rename from lib/ruby_ui/command/command_docs.rb rename to docs/app/views/docs/command.rb diff --git a/docs/app/views/docs/components.rb b/docs/app/views/docs/components.rb new file mode 100644 index 00000000..9adea7ba --- /dev/null +++ b/docs/app/views/docs/components.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Views::Docs::Components < Views::Base + include Components::Shared::ComponentsList + + def view_template + div(class: "mx-auto max-w-[800px] py-10") do + h1(class: "scroll-m-24 text-3xl font-semibold tracking-tight sm:text-3xl mb-4") { "Components" } + p(class: "text-lg text-foreground mb-8 text-balance") { "A UI component library, crafted precisely for Ruby devs who want to stay organised and build modern apps, fast." } + + div(class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-y-4 mb-24 mt-12") do + components.each do |component| + a(href: component[:path], class: "text-base text-foreground/80 hover:text-foreground hover:underline transition-colors") do + component[:name] + end + end + end + + div(class: "rounded-xl border border-muted bg-muted/20 p-6") do + h3(class: "font-semibold text-lg tracking-tight mb-2") { "Missing a component?" } + p(class: "text-foreground mb-4") do + plain "Can't find the component you're looking for? Let us know what you'd like to see next by opening a suggestion on our " + a(href: "https://github.com/ruby-ui/ruby_ui/issues/new", target: "_blank", class: "font-medium underline underline-offset-4") { "GitHub Issues" } + plain " page." + end + end + end + end +end diff --git a/lib/ruby_ui/context_menu/context_menu_docs.rb b/docs/app/views/docs/context_menu.rb similarity index 100% rename from lib/ruby_ui/context_menu/context_menu_docs.rb rename to docs/app/views/docs/context_menu.rb diff --git a/docs/app/views/docs/date_picker.rb b/docs/app/views/docs/date_picker.rb new file mode 100644 index 00000000..c50ece0c --- /dev/null +++ b/docs/app/views/docs/date_picker.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class Views::Docs::DatePicker < Views::Base + def view_template + component = "DatePicker" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Date Picker", description: "A date picker component with input.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Single Date", context: self) do + <<~RUBY + div(class: 'space-y-4 w-[260px]') do + Popover(options: { trigger: 'click' }) do + PopoverTrigger(class: 'w-full') do + div(class: 'grid w-full max-w-sm items-center gap-1.5') do + label(for: "date") { "Select a date" } + Input(type: 'string', placeholder: "Select a date", class: 'rounded-md border shadow', id: 'date', data_controller: 'ruby-ui--calendar-input') + end + end + PopoverContent do + Calendar(input_id: '#date') + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/dialog/dialog_docs.rb b/docs/app/views/docs/dialog.rb similarity index 100% rename from lib/ruby_ui/dialog/dialog_docs.rb rename to docs/app/views/docs/dialog.rb diff --git a/lib/ruby_ui/dropdown_menu/dropdown_menu_docs.rb b/docs/app/views/docs/dropdown_menu.rb similarity index 100% rename from lib/ruby_ui/dropdown_menu/dropdown_menu_docs.rb rename to docs/app/views/docs/dropdown_menu.rb diff --git a/lib/ruby_ui/form/form_docs.rb b/docs/app/views/docs/form.rb similarity index 100% rename from lib/ruby_ui/form/form_docs.rb rename to docs/app/views/docs/form.rb diff --git a/docs/app/views/docs/getting_started/customizing_components.rb b/docs/app/views/docs/getting_started/customizing_components.rb new file mode 100644 index 00000000..82a5678a --- /dev/null +++ b/docs/app/views/docs/getting_started/customizing_components.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +class Views::Docs::GettingStarted::CustomizingComponents < Views::Base + def view_template + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Customizing components", description: "When theming doesn't suffice, RubyUI allows you to tailor the components to your specific needs.") + + div(class: "space-y-4") do + Heading(level: 2) { "Introduction" } + Text { "While theming provides a powerful tool for modifying aspects such as fonts, brand colors, and border attributes, there may be instances where you need to directly customize the components. RubyUI is designed to facilitate this process with ease." } + end + + div(class: "space-y-4") do + Heading(level: 2) { "Updating attributes & classes" } + Text do + plain "All components accept any HTML attribute, and will pass it through to the underlying HTML element. This is great for quick changes, or when you need to add a custom class for a one off situation." + end + Text(size: "4", weight: "semibold") { "Adding attributes" } + Text do + plain "By default, attribute values are added to the existing values of the component. For instance, if you want to make a button span the full width of its container, you can do it like this: " + InlineCode { "Button(class: 'w-full')" } + plain ". This will add the " + InlineCode { "w-full" } + plain " class to the button, causing it to span the full width of its container." + end + Text(size: "4", weight: "semibold") { "Overriding Classes" } + Text do + plain "There might be instances where you need to override a specific style attribute. For instance, if you wish to alter the color of a button while keeping the rest of the styles intact, you can achieve this as follows: " + InlineCode { "Button(class: '!bg-red-500')" } + plain ". This will replace the default background color with red by utilizing the " + InlineCode { "!important" } + plain " modifier." + end + Text(size: "4", weight: "semibold") { "Replacing Attributes" } + Text do + plain "In some rare cases, you might need to replace the default value of an attribute entirely. For instance, if you want to change all the styles of a button, you can do it as follows: " + InlineCode { "Button(class!: 'bg-red-500 text-white py-2 px-4 font-medium hover:bg-red-600')" } + plain ". This will override all the default classes and apply only the ones you specify. In this case, it will apply " + InlineCode { "bg-red-500 text-white py-2 px-4 font-medium hover:bg-red-600" } + plain "." + end + end + + div(class: "space-y-4") do + Heading(level: 2) { "Redefining components" } + Text { "Redefining components is a powerful concept that allows you to redefine the underlying components through inheritance, or completely. This is useful when you need to make more complex changes to the components, that can't be achieved with attributes and classes alone. In other words, you can change the whole damn thing if you like." } + # to redefine a component, find the component you want to redefine in the source code, and copy it into your application. Then, make the changes you need. For example, if you want to change the button component, you can copy the button component from the source code, and paste it into your application. Then, you can make the changes you need. + Text(size: "4", weight: "semibold") { "How it works" } + Text do + plain "To redefine a component, find the component you want to redefine in the source code, and copy it into your application. Then, make the changes you need. For example, if you want to change the button component, you can copy the button component from the source code, and paste it into your application. Then, you can make the changes you need." + end + Text(size: "4", weight: "semibold") { "Let's redefine the Alert component" } + Text do + plain "Let's say you want to change the alert component to use a particular icon every time it is rendered. You can do this by redefining the component as follows:" + end + render Steps::Builder.new do |steps| + # Find source code + steps.add_step do + Heading(level: 4) { "Find the source code" } + Text do + plain "First, find the source code for the component you want to redefine. In this case, we want to redefine the " + InlineCode { "Alert" } + plain " component, so we'll find the source code for the alert component " + InlineLink(href: "https://github.com/ruby-ui/ruby_ui/blob/main/gem/lib/ruby_ui/alert.rb") { "here on Github" } + plain "." + end + end + # Copy source code to application + steps.add_step do + Heading(level: 4) { "Copy the source code" } + Text do + plain "Next, copy the source code for the component into your application. You can do this by creating a new file at " + InlineCode { "app/views/components/phlex_u_i/alert.rb" } + plain ", and pasting the source code into it. When using the " + InlineCode { "phlex-rails" } + plain " gem, all components are loaded from this directory. So if you want to redefine a component, it will always sit somewhere inside the " + InlineCode { "app/views/components/phlex_u_i" } + plain " directory." + end + Text do + plain "Your new file should look something like this:" + end + Codeblock(alert_component_definition, syntax: :ruby) + end + # Make changes + steps.add_step do + Heading(level: 4) { "Edit to perfection!" } + end + end + end + + div(class: "space-y-4") do + Heading(level: 2) { "Learning from shadcn/ui" } + Text do + plain "RubyUI components are inspired by " + InlineLink(href: "https://ui.shadcn.com") { "shadcn/ui" } + plain ", which has an extensive collection of beautifully designed components. When customizing RubyUI components, we recommend checking the " + InlineLink(href: "https://ui.shadcn.com/docs/components") { "shadcn/ui component documentation" } + plain " for reference on Tailwind CSS class implementations." + end + Text do + plain "shadcn/ui provides excellent examples of:" + end + Components.TypographyList do + Components.TypographyListItem { "Tailwind CSS class patterns for common UI elements" } + Components.TypographyListItem { "Accessibility best practices" } + Components.TypographyListItem { "Responsive design patterns" } + Components.TypographyListItem { "Dark mode implementations" } + end + end + + div(class: "space-y-4") do + Heading(level: 2) { "Contributing new components" } + Text do + plain "If you've implemented a component locally that exists in " + InlineLink(href: "https://ui.shadcn.com/docs/components") { "shadcn/ui" } + plain " but isn't yet available in RubyUI, we'd love for you to contribute it back to the community!" + end + Text do + plain "To contribute a new component:" + end + Components.TypographyList(numbered: true) do + Components.TypographyListItem do + plain "Check the " + InlineLink(href: "https://github.com/ruby-ui/ruby_ui") { "RubyUI GitHub repository" } + plain " to see if the component is already planned" + end + Components.TypographyListItem { "Open an issue or discussion to propose the new component" } + Components.TypographyListItem { "Submit a pull request with your implementation" } + end + Text do + plain "By contributing, you help expand the RubyUI ecosystem and make it easier for other Ruby developers to build beautiful interfaces." + end + end + end + end + + private + + def alert_component_definition + <<~RUBY + # frozen_string_literal: true + + module RubyUI + class Alert < Base + def initialize(variant: nil, **attrs) + @variant = variant + super(**attrs) # must be called after variant is set + end + + def view_template(&block) + div(**attrs, &block) + end + + private + + def colors + case @variant + when nil then 'ring-border bg-muted/20 text-foreground [&>svg]:opacity-80' + when :warning then 'ring-warning/20 bg-warning/5 text-warning [&>svg]:text-warning/80' + when :success then 'ring-success/20 bg-success/5 text-success [&>svg]:text-success/80' + when :destructive then 'ring-destructive/20 bg-destructive/5 text-destructive [&>svg]:text-destructive/80' + end + end + + def default_attrs + base_classes = 'backdrop-blur relative w-full ring-1 ring-inset rounded-lg px-4 py-4 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg~*]:pl-8' + { + class: [base_classes, colors], + } + end + end + end + RUBY + end + + private + + def puzzle_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 01-.657.643 48.39 48.39 0 01-4.163-.3c.186 1.613.293 3.25.315 4.907a.656.656 0 01-.658.663v0c-.355 0-.676-.186-.959-.401a1.647 1.647 0 00-1.003-.349c-1.036 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401v0c.31 0 .555.26.532.57a48.039 48.039 0 01-.642 5.056c1.518.19 3.058.309 4.616.354a.64.64 0 00.657-.643v0c0-.355-.186-.676-.401-.959a1.647 1.647 0 01-.349-1.003c0-1.035 1.008-1.875 2.25-1.875 1.243 0 2.25.84 2.25 1.875 0 .369-.128.713-.349 1.003-.215.283-.4.604-.4.959v0c0 .333.277.599.61.58a48.1 48.1 0 005.427-.63 48.05 48.05 0 00.582-4.717.532.532 0 00-.533-.57v0c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.035 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.37 0 .713.128 1.003.349.283.215.604.401.96.401v0a.656.656 0 00.658-.663 48.422 48.422 0 00-.37-5.36c-1.886.342-3.81.574-5.766.689a.578.578 0 01-.61-.58v0z" + ) + end + end +end diff --git a/docs/app/views/docs/getting_started/dark_mode.rb b/docs/app/views/docs/getting_started/dark_mode.rb new file mode 100644 index 00000000..43eb84f3 --- /dev/null +++ b/docs/app/views/docs/getting_started/dark_mode.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +class Views::Docs::GettingStarted::DarkMode < Views::Base + def view_template + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Dark mode", description: "How to use dark mode in your application") + + div(class: "space-y-4") do + heading2 { "How it works" } + Text { "RubyUI seamlessly integrates dark mode, a crucial feature for modern applications, enhancing user experience and catering to diverse user preferences." } + Text do + plain "RubyUI is setup to use the " + InlineLink(href: "https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually") { "TailwindCSS 'class' strategy" } + plain ". This means that you can toggle dark mode by adding or removing the " + InlineCode { "dark" } + plain " class from the " + InlineCode { "" } + plain " element." + end + Text { "To enable dark mode, follow the installation below." } + end + + div(class: "space-y-4") do + heading2 { "Installation" } + Text do + plain "To implement Dark mode, add the " + InlineCode { "ThemeToggle" } + plain " component (below) to your application layout file. This ensures it's available on all pages." + end + Text do + plain "This component is a button that toggles the " + InlineCode { "dark" } + plain " class on the " + InlineCode { "" } + plain " element, using the " + InlineCode { "ToggleThemeController" } + plain " Stimulus controller." + end + Alert do + AlertTitle { "Pro tip" } + AlertDescription do + plain "You can hide the theme toggle on specific pages, like so: " + InlineCode { "ThemeToggle(class: 'hidden')" } + plain "." + end + end + + div(class: "pt-4") do + render Docs::VisualCodeExample.new(title: "Toggle component", context: self) do + <<~RUBY + ThemeToggle do |toggle| + SetLightMode do + Button(variant: :outline, icon: true) do + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-4 h-4" + ) do |s| + s.path( + d: + "M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z" + ) + end + end + end + + SetDarkMode do + Button(variant: :outline, icon: true) do + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-4 h-4" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6a9 9 0 009 9 8.97 8.97 0 003.463-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z", + clip_rule: "evenodd" + ) + end + end + end + end + RUBY + end + end + end + end + end + + def heading2(&) + Heading(level: 2, class: "!text-2xl pb-4 border-b", &) + end + + def space_y_4(&) + div(class: "space-y-4", &) + end + + def space_y_2(&) + div(class: "space-y-2", &) + end +end diff --git a/docs/app/views/docs/getting_started/installation.rb b/docs/app/views/docs/getting_started/installation.rb new file mode 100644 index 00000000..cad2a34f --- /dev/null +++ b/docs/app/views/docs/getting_started/installation.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +class Views::Docs::GettingStarted::Installation < Views::Base + include DeferredRender + + def view_template + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Installation", description: "How to install dependencies and structure your app.") + + Heading(level: 2) { "Select a Framework" } + div(class: "grid grid-cols-1 sm:grid-cols-2 gap-4") do + framework_card(title: "Rails --- JS Bundler", link: docs_installation_rails_bundler_path) { rails_logo } + framework_card(title: "Rails --- Importmaps", link: docs_installation_rails_importmaps_path) { rails_logo } + end + end + end + + def framework_card(title:, link:, &block) + a(href: link) do + Card(class: "flex flex-col items-center gap-y-4 p-6 py-10 hover:bg-accent hover:text-accent-foreground transition-colors duration-200 ease-in-out") do + block.call + p(class: "text-lg font-medium") { title } + end + end + end + + def rails_logo + svg( + xmlns: "http://www.w3.org/2000/svg", + id: "Layer_1", + viewbox: "0 0 395.9 139.2", + fill: "currentColor", + class: "w-20" + ) do |s| + s.path( + class: "st0", + d: + "M344.6 121.1v18.1h32.7c6.7 0 18.2-4.9 18.6-18.6v-7c0-11.7-9.6-18.6-18.6-18.6H361v-8.4h32.3V68.4h-31c-8 0-18.7 6.6-18.7 18.9v6.3c0 12.3 10.6 18.6 18.7 18.6 22.5.1-5.4 0 15.4 0v8.8m-208.3-4.3s17.5-1.5 17.5-24.1-21.2-24.7-21.2-24.7h-38.2v71.3h19.2V122l16.6 17.2h28.4l-22.3-22.5zm-7.4-14.6h-15.3V85.8h15.4s4.3 1.6 4.3 8.1-4.4 8.2-4.4 8.2zm72.3-33.7h-19.5c-13.9 0-18.6 12.6-18.6 18.6v52.2h19.5v-12.5H234v12.5h18.9V87c0-15.2-13.8-18.6-18.6-18.6zm-.3 38.1h-18.4V89.2s0-3.9 6.1-3.9h6.7c5.4 0 5.5 3.9 5.5 3.9v17.3h.1zM261.8 68.4h20.3v70.8h-20.3zM310.6 120.9V68.4h-20.2v70.8h47.5v-18.3z" + ) + s.path( + class: "st0", + d: + "M7 139.2h79s-15.1-68.9 34.9-96.8c10.9-5.3 45.6-25.1 102.4 16.9 1.8-1.5 3.5-2.7 3.5-2.7s-52-51.9-109.9-46.1C87.8 13.1 52 39.6 31 74.6S7 139.2 7 139.2z" + ) + s.path( + class: "st0", + d: + "M7 139.2h79s-15.1-68.9 34.9-96.8c10.9-5.3 45.6-25.1 102.4 16.9 1.8-1.5 3.5-2.7 3.5-2.7s-52-51.9-109.9-46.1C87.8 13.1 52 39.6 31 74.6S7 139.2 7 139.2z" + ) + s.path( + class: "st0", + d: + "M7 139.2h79s-15.1-68.9 34.9-96.8c10.9-5.3 45.6-25.1 102.4 16.9 1.8-1.5 3.5-2.7 3.5-2.7s-52-51.9-109.9-46.1c-29.2 2.6-65 29.1-86 64.1S7 139.2 7 139.2zM171.6 16.1l.4-6.7c-.9-.5-3.4-1.7-9.7-3.5l-.4 6.6c3.3 1.1 6.5 2.3 9.7 3.6z" + ) + s.path( + class: "st0", + d: + "M162.1 37.3l-.4 6.3c3.3.1 6.6.5 9.9 1.2l.4-6.2c-3.4-.7-6.7-1.1-9.9-1.3zm-37-31.2h1l-2-6.1c-3.1 0-6.3.2-9.6.6l1.9 5.9c2.9-.3 5.8-.4 8.7-.4zm4.8 36.8l2.3 6.9c2.9-1.4 5.8-2.6 8.7-3.5l-2.2-6.6c-3.4 1-6.3 2.1-8.8 3.2zM84.5 16.6L80 9.7c-2.5 1.3-5.1 2.7-7.8 4.3l4.6 7c2.6-1.6 5.1-3.1 7.7-4.4zm20.5 45l4.8 7.2c1.7-2.5 3.7-4.8 5.9-7.1l-4.5-6.8c-2.3 2.1-4.4 4.4-6.2 6.7zM90.5 93.8l8.1 6.4c.4-3.9 1.1-7.8 2.1-11.7l-7.2-5.7c-1.3 3.7-2.2 7.4-3 11zM46.7 46.3l-7.1-6.2c-2.6 2.5-5.1 5-7.4 7.5l7.7 6.6c2.1-2.7 4.4-5.4 6.8-7.9zM16.5 91L5 86.8c-1.9 4.3-4 9.3-5 12l11.5 4.2c1.3-3.4 3.4-8.3 5-12zM89 119.2c.2 5.3.7 9.6 1.2 12.6l12 4.3c-.9-3.9-1.8-8.3-2.4-13L89 119.2z" + ) + end + end +end diff --git a/docs/app/views/docs/getting_started/introduction.rb b/docs/app/views/docs/getting_started/introduction.rb new file mode 100644 index 00000000..7eaa124d --- /dev/null +++ b/docs/app/views/docs/getting_started/introduction.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +class Views::Docs::GettingStarted::Introduction < Views::Base + def view_template + div(class: "max-w-2xl mx-auto w-full py-4 space-y-10") do + render Docs::Header.new(title: "Introduction", description: "Reusable UI components for Ruby developers") + + div(class: "space-y-4") do + iframe(width: "100%", height: "400", src: "https://www.youtube.com/embed/OQZam7rug00?si=JmZNzS5u194Q0AWQ", title: "YouTube video player", frameborder: "0", class: "rounded-xl border shadow-lg mb-10", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowfullscreen: true) + Heading(level: 2) { "About" } + Text do + plain "RubyUI is a UI framework for Ruby developers, built on top of " + InlineLink(href: "http://phlex.fun") { "Phlex" } + plain ", " + InlineLink(href: "https://tailwindcss.com") { "TailwindCSS" } + plain " and " + InlineLink(href: "https://stimulus.hotwire.dev") { "Stimulus JS" } + plain ". It provides a set of components that are easy to use, and easy to customize." + end + end + + div(class: "space-y-4") do + Heading(level: 2) { "Core ingredients" } + Text { "RubyUI is built on top of 3 core ingredients: " } + Components.TypographyList do + Components.TypographyListItem(class: "space-y-2") do + span(class: "font-bold") { "Phlex" } + plain " - A framework for building fast, reusable, testable views in pure Ruby." + end + Components.TypographyListItem(class: "space-y-2") do + span(class: "font-bold") { "TailwindCSS" } + plain " - A utility-first CSS framework for rapidly building custom designs." + end + Components.TypographyListItem(class: "space-y-2") do + span(class: "font-bold") { "Stimulus JS" } + plain " - A modest JavaScript framework for the HTML you already have." + end + end + end + + div(class: "space-y-4") do + Heading(level: 2) { "Design inspiration" } + Text do + plain "RubyUI's component designs are heavily inspired by " + InlineLink(href: "https://ui.shadcn.com") { "shadcn/ui" } + plain ", a beautifully designed collection of React components built on Tailwind CSS. shadcn/ui describes itself as \"The Foundation for your Design System\" - a set of components you can customize, extend, and make your own." + end + Text do + plain "We borrow design patterns, component APIs, and Tailwind CSS classes from shadcn/ui, bringing the same beautiful aesthetic to Ruby developers. This means:" + end + Components.TypographyList do + Components.TypographyListItem do + span(class: "font-medium") { "Same visual design " } + plain "- Components look and feel like their shadcn/ui counterparts" + end + Components.TypographyListItem do + span(class: "font-medium") { "Compatible theming " } + plain "- Use the same CSS variables and copy themes directly from shadcn/ui" + end + Components.TypographyListItem do + span(class: "font-medium") { "Familiar patterns " } + plain "- If you've used shadcn/ui, you'll feel right at home" + end + end + end + + div(class: "pt-10 border-t space-y-8") do + Heading(level: 1) { "Notes from the original author" } + + div(class: "flex items-center gap-3") do + Avatar(size: :md, class: "border") { img(src: "https://i.pravatar.cc/150?u=george", alt: "George Kettle") } + div do + p(class: "text-sm font-medium leading-none") { "George Kettle" } + p(class: "text-sm text-muted-foreground") { "Original author of RubyUI" } + end + end + + div(class: "space-y-4") do + Heading(level: 2) { "Why I built RubyUI" } + Text do + plain "Many Ruby developers are familiar with " + InlineLink(href: "https://rubyonrails.org") { "Rails" } + plain ", and the " + InlineLink(href: "https://guides.rubyonrails.org/layouts_and_rendering.html") { "convention over configuration" } + plain " approach it takes. RubyUI is built on the same principles, providing a set of components that are easy to use, and easy to customize." + end + Text do + plain "RubyUI was born out of a desire for a comprehensive UI framework designed with Ruby developers in mind. While I've previously utilized TailwindUI and other solutions, none seemed to fit just right. The plethora of UI component libraries available for JavaScript frameworks highlighted a gap in the Ruby ecosystem, which RubyUI aims to fill." + end + Text do + plain "Upon discovering Phlex, it became clear that it was the ideal foundation for such a library. It offered the potential for a powerful, easy-to-use, and customizable component library when paired with StimulusJS. The goal was to create a tool that leverages the strengths of TailwindCSS and StimulusJS, providing Ruby developers with a comprehensive UI solution that is stylable at the HTML level." + end + end + + div(class: "space-y-4") do + Heading(level: 2) { "Goals of RubyUI" } + Components.TypographyList(numbered: true) do + Components.TypographyListItem { "Create a reusable UI component library specifically for Ruby devs" } + Components.TypographyListItem { "Enable Ruby devs to create custom and complex UIs without needing to write CSS or JS" } + end + end + + div(class: "space-y-4") do + Heading(level: 2) { "My experience using Phlex" } + Text do + span(class: "font-medium") { "I was initially skeptical about Phlex. " } + plain "I worried about using an abstraction layer on top of HTML and thought this would be a bad move. However, after trying it I realised that I was wrong, and " + span(class: "font-medium") { "I know others who have had the same experience as myself" } + plain "." + end + Text { "After some time using Phlex, it's obvious to me that this is a better way to render your views in Ruby apps. Phlex is intuitive and simple. It is also incredibly fast (12x faster than ERB, 5x faster than ViewComponent), it also makes your code more organised and leads to a better developer experience." } + Text(size: "4", weight: "semibold") { "Same same, but different" } + Text do + plain "Phlex is essentially just HTML in Ruby form, bundled into a component. It's a simple concept, but it's incredibly powerful. It allows you to write your views in pure Ruby, without the need for a templating language. This means you can use all the features of Ruby, including loops, conditionals, and more." + end + Text do + plain "As an example, if you want to render a " + InlineCode { "

Phlex. Same same, but different.

" } + plain " element, you can do it like this " + InlineCode { "p(class: 'text-sm font-muted-foreground') { 'Phlex. Same same, but different.' }" } + plain "." + end + Text do + plain "This is a simple example, but it's easy to see how this can be scaled up to more complex views. " + span(class: "font-medium") { "It's only natural that we use logic to build HTML" } + plain ". Phlex simplifies this process, making it easier to convert data structures into HTML." + end + end + + div(class: "space-y-4") do + Heading(level: 2) { "Acknowledgments" } + Text { "I'd like to thank the following projects and people for helping me build RubyUI" } + Components.TypographyList do + Components.TypographyListItem do + InlineLink(href: "https://github.com/joeldrapper") { "Joel Drapper" } + plain " - Thanks for creating Phlex, and for your support and advice." + end + Components.TypographyListItem do + InlineLink(href: "https://phlex.fun") { "Phlex" } + plain " - The foundation of RubyUI." + end + Components.TypographyListItem do + InlineLink(href: "https://ui.shadcn.com") { "shadcn/ui" } + plain " - The design inspiration for RubyUI's components and theming system." + end + Components.TypographyListItem do + InlineLink(href: "https://stimulus.hotwired.dev") { "Stimulus JS" } + plain " - A quicker way to write JavaScript." + end + Components.TypographyListItem do + InlineLink(href: "http://tailwindcss.com") { "TailwindCSS" } + plain " - I wouldn't build without it." + end + Components.TypographyListItem do + InlineLink(href: "https://twitter.com/george_kettle") { "My Twitter followers" } + plain " - Thanks for all the ideas, feedback and support." + end + end + end + end + end + end +end diff --git a/docs/app/views/docs/getting_started/theming.rb b/docs/app/views/docs/getting_started/theming.rb new file mode 100644 index 00000000..12a2affb --- /dev/null +++ b/docs/app/views/docs/getting_started/theming.rb @@ -0,0 +1,296 @@ +# frozen_string_literal: true + +class Views::Docs::GettingStarted::Theming < Views::Base + def view_template + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Theming", description: "Using CSS variables for theming.") + + div(class: "space-y-4") do + Components.Heading(level: 2) { "Introduction" } + Text do + plain "RubyUI uses CSS Variables like " + InlineCode { "--primary: oklch(0.205 0 0)" } + plain " for theming. This approach is inspired by " + InlineLink(href: "https://ui.shadcn.com") { "shadcn/ui" } + plain " and allows you to easily customize the look and feel of your application." + end + # List the 2 benefits. That we can use CSS variables to change the style, without changing the tailwindcss classes used + # And that we can change the style of a particular tailwindcss class for both light and dark mode, without having to duplicate the tailwindcss class + # For instance, bg-primary will work for both light and dark mode, without having to define both bg-primary and dark:bg-primary-dark (or something else like that) + Text do + plain "There are " + span(class: "font-medium") { "two main benefits" } + plain " to this approach:" + end + Components.TypographyList do + Components.TypographyListItem do + span(class: "font-medium") { "Easily customisable design " } + plain "by updating CSS variables, without having to update the RubyUI component." + end + Components.TypographyListItem do + span(class: "font-medium") { "Simpler implementation " } + plain " for both light and dark mode, by not having to duplicate the TailwindCSS class (e.g. " + InlineCode { "bg-primary" } + plain " will work for both light and dark mode, without having to define both " + InlineCode { "bg-primary" } + plain " and " + InlineCode { "dark:bg-primary-dark" } + plain " - Or something else like that)." + end + end + end + + div(class: "space-y-4") do + Heading(level: 2) { "Convention" } + Text do + plain "We use a simple " + InlineCode { "background" } + plain " and " + InlineCode { "foreground" } + plain " convention for colors. The " + InlineCode { "background" } + plain " variable is used for the background color of the component and the " + InlineCode { "foreground" } + plain " variable is used for the text color. This is similar to other component libraries that are popular in React and elsewhere, and it works well in our experience." + end + Alert(class: "bg-transparent") do + AlertDescription do + plain "The " + InlineCode { "background" } + plain " suffix is omitted when the variable is used for the background color of the component." + end + end + Text { "Given the following CSS variables:" } + code = <<~CODE + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + CODE + Codeblock(code, syntax: :css) + Text do + plain "The " + InlineCode { "background" } + plain " color of the following component will be " + InlineCode { "var(--primary)" } + plain " and the " + InlineCode { "foreground" } + plain " color will be " + InlineCode { "var(--primary-foreground)" } + plain "." + end + code = <<~CODE +
We love Ruby
+ CODE + Codeblock(code, syntax: :html) + Alert(class: "bg-transparent") do + AlertDescription do + span(class: "font-medium") { "RubyUI uses oklch color format" } + plain ", the same format used by shadcn/ui. See the " + InlineLink(href: "https://tailwindcss.com/docs/customizing-colors#using-css-variables") { "Tailwind CSS documentation" } + plain " for more information." + end + end + end + + div(class: "space-y-4") do + Heading(level: 2) { "List of variables" } + Text { "Here's the list of variables available for customization:" } + Card(class: "space-y-4 shadow-none p-4 md:p-6") do + css_variables + end + end + + div(class: "space-y-4") do + Heading(level: 2) { "Adding new colors" } + Text do + plain "To add new colors, you need to add them to your " + InlineCode { "application.tailwind.css" } + plain " file and to your " + InlineCode { "tailwind.config.js" } + plain " file." + end + adding_a_color + end + + div(class: "space-y-4") do + Heading(level: 2) { "Color format (oklch)" } + Text do + plain "RubyUI uses " + InlineLink(href: "https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch") { "oklch colors" } + plain " as the default color format. This is the same format used by " + InlineLink(href: "https://ui.shadcn.com") { "shadcn/ui" } + plain " and provides better perceptual uniformity and wider color gamut support." + end + Text do + plain "While " + InlineCode { "oklch" } + plain " is recommended, you can also use other color formats such as " + InlineCode { "hsl" } + plain ", " + InlineCode { "rgb" } + plain ", or " + InlineCode { "rgba" } + plain ". See the " + InlineLink(href: "https://tailwindcss.com/docs/customizing-colors#using-css-variables") { "Tailwind CSS documentation" } + plain " for more information." + end + end + + div(class: "space-y-4") do + Heading(level: 2) { "shadcn/ui themes" } + Text do + plain "RubyUI themes use the same CSS variable convention as " + InlineLink(href: "https://ui.shadcn.com") { "shadcn/ui" } + plain ". This means you can copy themes directly from " + InlineLink(href: "https://ui.shadcn.com/themes") { "shadcn/ui themes" } + plain " and use them in your RubyUI application." + end + Text do + plain "Visit the " + InlineLink(href: "/themes/default") { "RubyUI themes page" } + plain " to preview and copy themes, just like you would on shadcn/ui." + end + end + end + end + + def css_variables + space_y_2 do + Text(size: "2", weight: "medium") { "Default background color of ...etc" } + code = <<~CODE + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + CODE + Codeblock(code, syntax: :css) + end + + space_y_2 do + Text(size: "2", weight: "medium") { "Muted backgrounds such as TabsList" } + code = <<~CODE + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + CODE + Codeblock(code, syntax: :css) + end + + space_y_2 do + Text(size: "2", weight: "medium") { "Default border color" } + code = <<~CODE + --border: oklch(0.922 0 0); + CODE + Codeblock(code, syntax: :css) + end + + space_y_2 do + Text(size: "2", weight: "medium") { "Border color for inputs such as Input, Select or Textarea" } + code = <<~CODE + --input: oklch(0.922 0 0); + CODE + Codeblock(code, syntax: :css) + end + + space_y_2 do + Text(size: "2", weight: "medium") { "Primary colors for Button" } + code = <<~CODE + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + CODE + Codeblock(code, syntax: :css) + end + + space_y_2 do + Text(size: "2", weight: "medium") { "Secondary colors for Button" } + code = <<~CODE + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + CODE + Codeblock(code, syntax: :css) + end + + space_y_2 do + Text(size: "2", weight: "medium") { "Used for accents such as hover effects on DropdownMenu::Item, Select::Item... etc" } + code = <<~CODE + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + CODE + Codeblock(code, syntax: :css) + end + + space_y_2 do + Text(size: "2", weight: "medium") { "Used for destructive actions such as Button.new(variant: :destructive)" } + code = <<~CODE + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + CODE + Codeblock(code, syntax: :css) + end + + space_y_2 do + Text(size: "2", weight: "medium") { "Used for focus ring" } + code = <<~CODE + --ring: oklch(0.708 0 0); + CODE + Codeblock(code, syntax: :css) + end + + space_y_2 do + Text(size: "2", weight: "medium") { "Border radius for card, input and buttons" } + code = <<~CODE + --radius: 0.625rem; + CODE + Codeblock(code, syntax: :css) + end + end + + def adding_a_color + space_y_2 do + Text(size: "2", weight: "medium") do + span(class: "text-muted-foreground") { "app/stylesheets/" } + plain "application.tailwind.css" + end + code = <<~CODE + :root { + --contrast: oklch(0.75 0.18 85); + --contrast-foreground: oklch(0.25 0.05 85); + } + + .dark { + --contrast: oklch(0.85 0.15 85); + --contrast-foreground: oklch(0.2 0.05 85); + } + CODE + Codeblock(code, syntax: :css) + end + + space_y_2 do + Text(size: "2", weight: "medium") { "application.tailwind.css (inside @theme inline)" } + code = <<~CODE + @theme inline { + --color-contrast: var(--contrast); + --color-contrast-foreground: var(--contrast-foreground); + } + CODE + Codeblock(code, syntax: :css) + end + + Text do + plain "You can now use the " + InlineCode { "contrast" } + plain " and " + InlineCode { "contrast-foreground" } + plain " variables in your application." + end + + code = <<~CODE +
We love Ruby
+ CODE + Codeblock(code, syntax: :html) + end + + def space_y_4(&) + div(class: "space-y-4", &) + end + + def space_y_2(&) + div(class: "space-y-2", &) + end +end diff --git a/docs/app/views/docs/hover_card.rb b/docs/app/views/docs/hover_card.rb new file mode 100644 index 00000000..75c1cb7d --- /dev/null +++ b/docs/app/views/docs/hover_card.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class Views::Docs::HoverCard < Views::Base + def view_template + component = "HoverCard" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Hover Card", description: "For sighted users to preview content available behind a link.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + HoverCard do + HoverCardTrigger do + Button(variant: :link) { "@joeldrapper" } # Make this a link in order to navigate somewhere + end + HoverCardContent do + div(class: "flex justify-between space-x-4") do + Avatar do + AvatarImage(src: "https://avatars.githubusercontent.com/u/246692?v=4", alt: "joeldrapper") + AvatarFallback { "JD" } + end + div(class: "space-y-1") do + h4(class: "text-sm font-medium") { "@joeldrapper" } + p(class: "text-sm") do + "Creator of Phlex Components. Ruby on Rails developer." + end + div(class: "flex items-center pt-2") do + svg( + width: "15", + height: "15", + viewbox: "0 0 15 15", + fill: "none", + xmlns: "http://www.w3.org/2000/svg", + class: "mr-2 h-4 w-4 opacity-70" + ) do |s| + s.path( + d: + "M4.5 1C4.77614 1 5 1.22386 5 1.5V2H10V1.5C10 1.22386 10.2239 1 10.5 1C10.7761 1 11 1.22386 11 1.5V2H12.5C13.3284 2 14 2.67157 14 3.5V12.5C14 13.3284 13.3284 14 12.5 14H2.5C1.67157 14 1 13.3284 1 12.5V3.5C1 2.67157 1.67157 2 2.5 2H4V1.5C4 1.22386 4.22386 1 4.5 1ZM10 3V3.5C10 3.77614 10.2239 4 10.5 4C10.7761 4 11 3.77614 11 3.5V3H12.5C12.7761 3 13 3.22386 13 3.5V5H2V3.5C2 3.22386 2.22386 3 2.5 3H4V3.5C4 3.77614 4.22386 4 4.5 4C4.77614 4 5 3.77614 5 3.5V3H10ZM2 6V12.5C2 12.7761 2.22386 13 2.5 13H12.5C12.7761 13 13 12.7761 13 12.5V6H2ZM7 7.5C7 7.22386 7.22386 7 7.5 7C7.77614 7 8 7.22386 8 7.5C8 7.77614 7.77614 8 7.5 8C7.22386 8 7 7.77614 7 7.5ZM9.5 7C9.22386 7 9 7.22386 9 7.5C9 7.77614 9.22386 8 9.5 8C9.77614 8 10 7.77614 10 7.5C10 7.22386 9.77614 7 9.5 7ZM11 7.5C11 7.22386 11.2239 7 11.5 7C11.7761 7 12 7.22386 12 7.5C12 7.77614 11.7761 8 11.5 8C11.2239 8 11 7.77614 11 7.5ZM11.5 9C11.2239 9 11 9.22386 11 9.5C11 9.77614 11.2239 10 11.5 10C11.7761 10 12 9.77614 12 9.5C12 9.22386 11.7761 9 11.5 9ZM9 9.5C9 9.22386 9.22386 9 9.5 9C9.77614 9 10 9.22386 10 9.5C10 9.77614 9.77614 10 9.5 10C9.22386 10 9 9.77614 9 9.5ZM7.5 9C7.22386 9 7 9.22386 7 9.5C7 9.77614 7.22386 10 7.5 10C7.77614 10 8 9.77614 8 9.5C8 9.22386 7.77614 9 7.5 9ZM5 9.5C5 9.22386 5.22386 9 5.5 9C5.77614 9 6 9.22386 6 9.5C6 9.77614 5.77614 10 5.5 10C5.22386 10 5 9.77614 5 9.5ZM3.5 9C3.22386 9 3 9.22386 3 9.5C3 9.77614 3.22386 10 3.5 10C3.77614 10 4 9.77614 4 9.5C4 9.22386 3.77614 9 3.5 9ZM3 11.5C3 11.2239 3.22386 11 3.5 11C3.77614 11 4 11.2239 4 11.5C4 11.7761 3.77614 12 3.5 12C3.22386 12 3 11.7761 3 11.5ZM5.5 11C5.22386 11 5 11.2239 5 11.5C5 11.7761 5.22386 12 5.5 12C5.77614 12 6 11.7761 6 11.5C6 11.2239 5.77614 11 5.5 11ZM7 11.5C7 11.2239 7.22386 11 7.5 11C7.77614 11 8 11.2239 8 11.5C8 11.7761 7.77614 12 7.5 12C7.22386 12 7 11.7761 7 11.5ZM9.5 11C9.22386 11 9 11.2239 9 11.5C9 11.7761 9.22386 12 9.5 12C9.77614 12 10 11.7761 10 11.5C10 11.2239 9.77614 11 9.5 11Z", + fill: "currentColor", + fill_rule: "evenodd", + clip_rule: "evenodd" + ) + end + span(class: "text-xs text-muted-foreground") { "Joined December 2021" } + end + end + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end + + private + + # def components + # [ + # Docs::ComponentStruct.new(name: "PopoverController", source: "https://github.com/ruby-ui/ruby_ui_stimulus/blob/main/controllers/popover_controller.js", built_using: :stimulus), + # Docs::ComponentStruct.new(name: "HoverCard", source: "https://github.com/ruby-ui/ruby_ui/blob/main/gem/lib/ruby_ui/hover_card.rb", built_using: :phlex), + # Docs::ComponentStruct.new(name: "HoverCardTrigger", source: "https://github.com/ruby-ui/ruby_ui/blob/main/gem/lib/ruby_ui/hover_card/trigger.rb", built_using: :phlex), + # Docs::ComponentStruct.new(name: "HoverCardContent", source: "https://github.com/ruby-ui/ruby_ui/blob/main/gem/lib/ruby_ui/hover_card/content.rb", built_using: :phlex) + # ] + # end +end diff --git a/lib/ruby_ui/input/input_docs.rb b/docs/app/views/docs/input.rb similarity index 100% rename from lib/ruby_ui/input/input_docs.rb rename to docs/app/views/docs/input.rb diff --git a/docs/app/views/docs/installation/rails_bundler.rb b/docs/app/views/docs/installation/rails_bundler.rb new file mode 100644 index 00000000..1eb40d90 --- /dev/null +++ b/docs/app/views/docs/installation/rails_bundler.rb @@ -0,0 +1,436 @@ +# frozen_string_literal: true + +class Views::Docs::Installation::RailsBundler < Views::Base + def view_template + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Rails - JS Bundler", description: "How to install RubyUI within a Rails app that employs JS bundling.") + + Alert(variant: :info) do + AlertTitle { "RubyUI" } + AlertDescription { "To take full advantage of RubyUI, the application is expected to be using TailwindCSS 4 and Stimulus" } + end + + Heading(level: 2, class: "!text-2xl pb-4 border-b") { "Using RubyUI CLI" } + Text do + "We provide a Ruby gem with useful generators to help you to setup RubyUI components in your apps." + end + + render Steps::Builder.new do |steps| + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + plain "Add RubyUI gem to your Gemfile" + end + + code = <<~CODE + bundle add ruby_ui --group development --require false + CODE + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + end + end + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Run the install command" + end + + code = <<~CODE + rails g ruby_ui:install + CODE + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + end + end + end + + Heading(level: 2, class: "!text-2xl pb-4 border-b") { "Manual" } + Text do + "You can install the dependencies manually if you prefer" + end + render Steps::Builder.new do |steps| + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Add Phlex Rails to your app" + end + + code = <<~CODE + bundle add phlex-rails + CODE + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + + Alert(variant: :warning) do + info_icon + AlertTitle { "Phlex compatibility" } + AlertDescription { "Note that RubyUI components target Phlex 2.x most recent version" } + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Install Phlex Rails" + end + + code = <<~CODE + bin/rails g phlex:install + CODE + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Install tailwind_merge" + end + + Text do + "RubyUI components use tailwind_merge to avoid conflicts between TailwindCSS classes" + end + + code = <<~CODE + bundle add tailwind_merge + CODE + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Create RubyUI initializer" + end + + Text do + plain "Add this code to " + InlineCode(class: "whitespace-nowrap") { "config/initializers/ruby_ui.rb" } + end + + code = <<~RUBY + module RubyUI + extend Phlex::Kit + end + + # Allow using RubyUI instead RubyUi + Rails.autoloaders.main.inflector.inflect( + "ruby_ui" => "RubyUI" + ) + + # Allow using RubyUI::ComponentName instead Components::RubyUI::ComponentName + Rails.autoloaders.main.push_dir( + "\#{Rails.root}/app/components/ruby_ui", namespace: RubyUI + ) + + # Allow using RubyUI::ComponentName instead RubyUI::ComponentName::ComponentName + Rails.autoloaders.main.collapse(Rails.root.join("app/components/ruby_ui/*")) + RUBY + div(class: "w-full") do + Codeblock(code, syntax: :ruby) + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Include RubyUI kit in your base component" + end + + Text do + plain "Include " + InlineCode(class: "whitespace-nowrap") { "RubyUI" } + plain " module in " + InlineCode(class: "whitespace-nowrap") { "app/components/base.rb" } + end + + code = <<~RUBY + module Components + class Base < Phlex::HTML + include Components + include RubyUI + + ... + RUBY + div(class: "w-full") do + Codeblock(code, syntax: :ruby) + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Create the RubyUI base component" + end + + Text do + plain "Every RubyUI component inherit from RubyUI::Base " + end + + Text do + plain "Copy and paste the code snippet below into the " + InlineCode(class: "whitespace-nowrap") { "app/components/ruby_ui/base.rb" } + plain " file." + end + + code = <<~RUBY + require "tailwind_merge" + + module RubyUI + class Base < Phlex::HTML + TAILWIND_MERGER = ::TailwindMerge::Merger.new.freeze unless defined?(TAILWIND_MERGER) + + attr_reader :attrs + + def initialize(**user_attrs) + @attrs = mix(default_attrs, user_attrs) + @attrs[:class] = TAILWIND_MERGER.merge(@attrs[:class]) if @attrs[:class] + end + + private + + def default_attrs + {} + end + end + end + RUBY + div(class: "w-full") do + Codeblock(code, syntax: :ruby) + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Include RubyUI configuration & styles in your CSS" + end + + Text do + plain "Include RubyUI styles in " + InlineCode(class: "whitespace-nowrap") { "app/assets/stylesheets/application.tailwind.css" } + end + + Text do + "Your CSS file will look like this:" + end + + code = <<~STYLESHEET + @import "tailwindcss"; + + @plugin "@tailwindcss/forms"; + @plugin "@tailwindcss/typography"; + + @import "tw-animate-css"; + + @custom-variant dark (&:is(.dark *)); + + :root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + + /* ruby_ui specific */ + --warning: hsl(38 92% 50%); + --warning-foreground: hsl(0 0% 100%); + --success: hsl(87 100% 37%); + --success-foreground: hsl(0 0% 100%); + } + + .dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); + + /* ruby_ui specific */ + --warning: hsl(38 92% 50%); + --warning-foreground: hsl(0 0% 100%); + --success: hsl(84 81% 44%); + --success-foreground: hsl(0 0% 100%); + } + + @theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + /* ruby_ui specific */ + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + } + + /* Container settings */ + @utility container { + margin-inline: auto; + padding-inline: 2rem; + max-width: 1400px; + } + + + @layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + } + + STYLESHEET + + div(class: "w-full") do + Codeblock(code, syntax: :css) + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Install tw-animate-css plugin" + end + + Text do + plain "Some RubyUI components utilize CSS animations to create smooth transitions." + end + + code = <<~CODE + yarn add tw-animate-css + CODE + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + end + end + end + end + end + + private + + def step_container(&) + div(class: "space-y-4", &) + end + + def info_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 01.67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 11-.671-1.34l.041-.022zM12 9a.75.75 0 100-1.5.75.75 0 000 1.5z", + clip_rule: "evenodd" + ) + end + end +end diff --git a/docs/app/views/docs/installation/rails_importmaps.rb b/docs/app/views/docs/installation/rails_importmaps.rb new file mode 100644 index 00000000..e0e3373c --- /dev/null +++ b/docs/app/views/docs/installation/rails_importmaps.rb @@ -0,0 +1,435 @@ +# frozen_string_literal: true + +class Views::Docs::Installation::RailsImportmaps < Views::Base + def view_template + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Rails - Importmap", description: "How to install RubyUI within a Rails app that employs import maps") + + Alert(variant: :info) do + AlertTitle { "RubyUI" } + AlertDescription { "To take full advantage of RubyUI, the application is expected to be using TailwindCSS 4 and Stimulus" } + end + + Heading(level: 2, class: "!text-2xl pb-4 border-b") { "Using RubyUI CLI" } + Text do + "We provide a Ruby gem with useful generators to help you to setup RubyUI components in your apps." + end + + render Steps::Builder.new do |steps| + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + plain "Add RubyUI gem to your Gemfile" + end + + code = <<~CODE + bundle add ruby_ui --group development --require false + CODE + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Run the install command" + end + + code = <<~CODE + rails g ruby_ui:install + CODE + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + end + end + end + + Heading(level: 2, class: "!text-2xl pb-4 border-b") { "Manual" } + Text do + "You can install the dependencies manually if you prefer" + end + render Steps::Builder.new do |steps| + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Add Phlex Rails to your app" + end + + code = <<~CODE + bundle add phlex-rails + CODE + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + + Alert(variant: :warning) do + info_icon + AlertTitle { "Phlex compatibility" } + AlertDescription { "Note that RubyUI components target Phlex 2.x most recent version" } + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Install Phlex Rails" + end + + code = <<~CODE + bin/rails g phlex:install + CODE + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Install tailwind_merge" + end + + Text do + "RubyUI components use tailwind_merge to avoid conflicts between TailwindCSS classes" + end + + code = <<~CODE + bundle add tailwind_merge + CODE + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Create RubyUI initializer" + end + + Text do + plain "Add this code to " + InlineCode(class: "whitespace-nowrap") { "config/initializers/ruby_ui.rb" } + end + + code = <<~RUBY + module RubyUI + extend Phlex::Kit + end + + # Allow using RubyUI instead RubyUi + Rails.autoloaders.main.inflector.inflect( + "ruby_ui" => "RubyUI" + ) + + # Allow using RubyUI::ComponentName instead Components::RubyUI::ComponentName + Rails.autoloaders.main.push_dir( + "\#{Rails.root}/app/components/ruby_ui", namespace: RubyUI + ) + + # Allow using RubyUI::ComponentName instead RubyUI::ComponentName::ComponentName + Rails.autoloaders.main.collapse(Rails.root.join("app/components/ruby_ui/*")) + RUBY + div(class: "w-full") do + Codeblock(code, syntax: :ruby) + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Include RubyUI kit in your base component" + end + + Text do + plain "Include " + InlineCode(class: "whitespace-nowrap") { "RubyUI" } + plain " module in " + InlineCode(class: "whitespace-nowrap") { "app/components/base.rb" } + end + + code = <<~RUBY + module Components + class Base < Phlex::HTML + include Components + include RubyUI + + ... + RUBY + div(class: "w-full") do + Codeblock(code, syntax: :ruby) + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Create the RubyUI base component" + end + + Text do + plain "Every RubyUI component inherit from RubyUI::Base " + end + + Text do + plain "Copy and paste the code snippet below into the " + InlineCode(class: "whitespace-nowrap") { "app/components/ruby_ui/base.rb" } + plain " file." + end + + code = <<~RUBY + require "tailwind_merge" + + module RubyUI + class Base < Phlex::HTML + TAILWIND_MERGER = ::TailwindMerge::Merger.new.freeze unless defined?(TAILWIND_MERGER) + + attr_reader :attrs + + def initialize(**user_attrs) + @attrs = mix(default_attrs, user_attrs) + @attrs[:class] = TAILWIND_MERGER.merge(@attrs[:class]) if @attrs[:class] + end + + private + + def default_attrs + {} + end + end + end + RUBY + div(class: "w-full") do + Codeblock(code, syntax: :ruby) + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Include RubyUI configuration & styles in your CSS" + end + + Text do + plain "Include RubyUI styles in " + InlineCode(class: "whitespace-nowrap") { "app/assets/tailwind/application.css" } + end + + Text do + "Your CSS file will look like this:" + end + + code = <<~STYLESHEET + @import "tailwindcss"; + + @plugin "@tailwindcss/forms"; + @plugin "@tailwindcss/typography"; + + @import "../../../vendor/javascript/tw-animate-css.js"; + + @custom-variant dark (&:is(.dark *)); + + :root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + + /* ruby_ui specific */ + --warning: hsl(38 92% 50%); + --warning-foreground: hsl(0 0% 100%); + --success: hsl(87 100% 37%); + --success-foreground: hsl(0 0% 100%); + } + + .dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); + + /* ruby_ui specific */ + --warning: hsl(38 92% 50%); + --warning-foreground: hsl(0 0% 100%); + --success: hsl(84 81% 44%); + --success-foreground: hsl(0 0% 100%); + } + + @theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + /* ruby_ui specific */ + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + } + + /* Container settings */ + @utility container { + margin-inline: auto; + padding-inline: 2rem; + max-width: 1400px; + } + + @layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + } + STYLESHEET + + div(class: "w-full") do + Codeblock(code, syntax: :css) + end + end + end + + steps.add_step do + step_container do + Text(size: "4", weight: "semibold") do + "Install tw-animate-css plugin" + end + + Text do + plain "Some RubyUI components utilize CSS animations to create smooth transitions." + end + + code = <<~CODE + bin/importmap pin tw-animate-css + CODE + div(class: "w-full") do + Codeblock(code, syntax: :javascript) + end + end + end + end + end + end + + private + + def step_container(&) + div(class: "space-y-4", &) + end + + def info_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 01.67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 11-.671-1.34l.041-.022zM12 9a.75.75 0 100-1.5.75.75 0 000 1.5z", + clip_rule: "evenodd" + ) + end + end +end diff --git a/lib/ruby_ui/link/link_docs.rb b/docs/app/views/docs/link.rb similarity index 100% rename from lib/ruby_ui/link/link_docs.rb rename to docs/app/views/docs/link.rb diff --git a/lib/ruby_ui/masked_input/masked_input_docs.rb b/docs/app/views/docs/masked_input.rb similarity index 100% rename from lib/ruby_ui/masked_input/masked_input_docs.rb rename to docs/app/views/docs/masked_input.rb diff --git a/lib/ruby_ui/native_select/native_select_docs.rb b/docs/app/views/docs/native_select.rb similarity index 100% rename from lib/ruby_ui/native_select/native_select_docs.rb rename to docs/app/views/docs/native_select.rb diff --git a/lib/ruby_ui/pagination/pagination_docs.rb b/docs/app/views/docs/pagination.rb similarity index 100% rename from lib/ruby_ui/pagination/pagination_docs.rb rename to docs/app/views/docs/pagination.rb diff --git a/lib/ruby_ui/popover/popover_docs.rb b/docs/app/views/docs/popover.rb similarity index 100% rename from lib/ruby_ui/popover/popover_docs.rb rename to docs/app/views/docs/popover.rb diff --git a/lib/ruby_ui/progress/progress_docs.rb b/docs/app/views/docs/progress.rb similarity index 100% rename from lib/ruby_ui/progress/progress_docs.rb rename to docs/app/views/docs/progress.rb diff --git a/lib/ruby_ui/radio_button/radio_button_docs.rb b/docs/app/views/docs/radio_button.rb similarity index 100% rename from lib/ruby_ui/radio_button/radio_button_docs.rb rename to docs/app/views/docs/radio_button.rb diff --git a/lib/ruby_ui/select/select_docs.rb b/docs/app/views/docs/select.rb similarity index 100% rename from lib/ruby_ui/select/select_docs.rb rename to docs/app/views/docs/select.rb diff --git a/lib/ruby_ui/separator/separator_docs.rb b/docs/app/views/docs/separator.rb similarity index 100% rename from lib/ruby_ui/separator/separator_docs.rb rename to docs/app/views/docs/separator.rb diff --git a/lib/ruby_ui/sheet/sheet_docs.rb b/docs/app/views/docs/sheet.rb similarity index 100% rename from lib/ruby_ui/sheet/sheet_docs.rb rename to docs/app/views/docs/sheet.rb diff --git a/lib/ruby_ui/shortcut_key/shortcut_key_docs.rb b/docs/app/views/docs/shortcut_key.rb similarity index 100% rename from lib/ruby_ui/shortcut_key/shortcut_key_docs.rb rename to docs/app/views/docs/shortcut_key.rb diff --git a/lib/ruby_ui/sidebar/sidebar_docs.rb b/docs/app/views/docs/sidebar.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_docs.rb rename to docs/app/views/docs/sidebar.rb diff --git a/docs/app/views/docs/sidebar/example.rb b/docs/app/views/docs/sidebar/example.rb new file mode 100644 index 00000000..ac5f2ec8 --- /dev/null +++ b/docs/app/views/docs/sidebar/example.rb @@ -0,0 +1,286 @@ +# frozen_string_literal: true + +class Views::Docs::Sidebar::Example < Views::Base + FAVORITES = [ + {name: "Project Management & Task Tracking", emoji: "📊"}, + {name: "Movies & TV Shows", emoji: "🎬"}, + {name: "Books & Articles", emoji: "📚"}, + {name: "Recipes & Meal Planning", emoji: "🍽️"}, + {name: "Travel & Places", emoji: "🌍"}, + {name: "Health & Fitness", emoji: "🏋️"} + ].freeze + + WORKSPACES = [ + {name: "Personal Life Management", emoji: "🏡"}, + {name: "Work & Projects", emoji: "💼"}, + {name: "Side Projects", emoji: "🚀"}, + {name: "Learning & Courses", emoji: "📚"}, + {name: "Writing & Blogging", emoji: "📝"}, + {name: "Design & Development", emoji: "🎨"} + ].freeze + + def initialize(sidebar_state:) + @sidebar_state = sidebar_state + end + + CODE = <<~RUBY + SidebarWrapper do + Sidebar(collapsible: :icon) do + SidebarHeader do + SidebarMenu do + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + search_icon() + span { "Search" } + end + end + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#", active: true) do + home_icon() + span { "Home" } + end + end + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + inbox_icon() + span { "Inbox" } + SidebarMenuBadge { 4 } + end + end + end + end + SidebarContent do + SidebarGroup do + SidebarGroupLabel { "Favorites" } + SidebarMenu do + FAVORITES.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + span { item[:emoji] } + span { item[:name] } + end + DropdownMenu(options: { strategy: "fixed", placement: "right-start" }) do + SidebarMenuAction( + data: { + ruby_ui__dropdown_menu_target: "trigger", + action: "click->ruby-ui--dropdown-menu#toggle" + } + ) do + ellipsis_icon() + span(class: "sr-only") { "More" } + end + DropdownMenuContent do + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + end + end + end + end + SidebarGroup do + SidebarGroupLabel { "Workspaces" } + SidebarMenu do + WORKSPACES.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + span { item[:emoji] } + span { item[:name] } + end + DropdownMenu() do + SidebarMenuAction( + data: { + ruby_ui__dropdown_menu_target: "trigger", + action: "click->ruby-ui--dropdown-menu#toggle" + } + ) do + ellipsis_icon() + span(class: "sr-only") { "More" } + end + DropdownMenuContent do + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + end + end + end + end + SidebarGroup(class: "mt-auto") do + SidebarGroupContent do + SidebarMenu do + nav_secondary.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + item[:icon].call + span { item[:label] } + end + end + end + end + end + end + end + SidebarRail() + end + SidebarInset do + header(class: "flex h-16 shrink-0 items-center gap-2 border-b px-4") do + SidebarTrigger(class: "-ml-1") + end + end + end + RUBY + + def view_template + decoded_code = CGI.unescapeHTML(CODE) + instance_eval(decoded_code) + end + + private + + def nav_secondary + [ + {label: "Settings", icon: -> { settings_icon }}, + {label: "Help & Support", icon: -> { message_circle_question }} + ] + end + + def home_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-house" + ) do |s| + s.path(d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8") + s.path(d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z") + end + end + + def inbox_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-inbox" + ) do |s| + s.polyline(points: "22 12 16 12 14 15 10 15 8 12 2 12") + s.path(d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z") + end + end + + def calendar_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-calendar" + ) do |s| + s.path(d: "M8 2v4") + s.path(d: "M16 2v4") + s.rect(width: "18", height: "18", x: "3", y: "4", rx: "2") + s.path(d: "M3 10h18") + end + end + + def search_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-search" + ) do |s| + s.circle(cx: "11", cy: "11", r: "8") + s.path(d: "M21 21L16.7 16.7") + end + end + + def settings_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-settings" + ) do |s| + s.path(d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73 + l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z") + s.circle(cx: "12", cy: "12", r: "3") + end + end + + def plus_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-plus" + ) do |s| + s.path(d: "M5 12h14") + s.path(d: "M12 5v14") + end + end + + def gallery_vertical_end + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", view_box: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_linejoin: "round", class: "lucide lucide-gallery-vertical-end size-4") do |s| + s.path d: "M7 2h10" + s.path d: "M5 6h14" + s.rect width: "18", height: "12", x: "3", y: "10", rx: "2" + end + end + + def ellipsis_icon + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_line_join: "round", class: "lucide lucide-ellipsis-icon lucide-ellipsis") do |s| + s.circle cx: "12", cy: "12", r: "1" + s.circle cx: "19", cy: "12", r: "1" + s.circle cx: "5", cy: "12", r: "1" + end + end + + def message_circle_question + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_line_join: "round", class: "lucide lucide-message-circle-question-icon lucide-message-circle-question") do |s| + s.path d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" + s.path d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" + s.path d: "M12 17h.01" + end + end +end diff --git a/docs/app/views/docs/sidebar/inset_example.rb b/docs/app/views/docs/sidebar/inset_example.rb new file mode 100644 index 00000000..08c9fbed --- /dev/null +++ b/docs/app/views/docs/sidebar/inset_example.rb @@ -0,0 +1,286 @@ +# frozen_string_literal: true + +class Views::Docs::Sidebar::InsetExample < Views::Base + FAVORITES = [ + {name: "Project Management & Task Tracking", emoji: "📊"}, + {name: "Movies & TV Shows", emoji: "🎬"}, + {name: "Books & Articles", emoji: "📚"}, + {name: "Recipes & Meal Planning", emoji: "🍽️"}, + {name: "Travel & Places", emoji: "🌍"}, + {name: "Health & Fitness", emoji: "🏋️"} + ].freeze + + WORKSPACES = [ + {name: "Personal Life Management", emoji: "🏡"}, + {name: "Work & Projects", emoji: "💼"}, + {name: "Side Projects", emoji: "🚀"}, + {name: "Learning & Courses", emoji: "📚"}, + {name: "Writing & Blogging", emoji: "📝"}, + {name: "Design & Development", emoji: "🎨"} + ].freeze + + def initialize(sidebar_state:) + @sidebar_state = sidebar_state + end + + CODE = <<~RUBY + SidebarWrapper do + Sidebar(variant: :inset) do + SidebarHeader do + SidebarMenu do + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + search_icon() + span { "Search" } + end + end + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#", active: true) do + home_icon() + span { "Home" } + end + end + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + inbox_icon() + span { "Inbox" } + SidebarMenuBadge { 4 } + end + end + end + end + SidebarContent do + SidebarGroup do + SidebarGroupLabel { "Favorites" } + SidebarMenu do + FAVORITES.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + span { item[:emoji] } + span { item[:name] } + end + DropdownMenu(options: { strategy: "fixed", placement: "right-start" }) do + SidebarMenuAction( + data: { + ruby_ui__dropdown_menu_target: "trigger", + action: "click->ruby-ui--dropdown-menu#toggle" + } + ) do + ellipsis_icon() + span(class: "sr-only") { "More" } + end + DropdownMenuContent do + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + end + end + end + end + SidebarGroup do + SidebarGroupLabel { "Workspaces" } + SidebarMenu do + WORKSPACES.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + span { item[:emoji] } + span { item[:name] } + end + DropdownMenu() do + SidebarMenuAction( + data: { + ruby_ui__dropdown_menu_target: "trigger", + action: "click->ruby-ui--dropdown-menu#toggle" + } + ) do + ellipsis_icon() + span(class: "sr-only") { "More" } + end + DropdownMenuContent do + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + end + end + end + end + SidebarGroup(class: "mt-auto") do + SidebarGroupContent do + SidebarMenu do + nav_secondary.each do |item| + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + item[:icon].call + span { item[:label] } + end + end + end + end + end + end + end + SidebarRail() + end + SidebarInset do + header(class: "flex h-16 shrink-0 items-center gap-2 border-b px-4") do + SidebarTrigger(class: "-ml-1") + end + end + end + RUBY + + def view_template + decoded_code = CGI.unescapeHTML(CODE) + instance_eval(decoded_code) + end + + private + + def nav_secondary + [ + {label: "Settings", icon: -> { settings_icon }}, + {label: "Help & Support", icon: -> { message_circle_question }} + ] + end + + def home_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-house" + ) do |s| + s.path(d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8") + s.path(d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z") + end + end + + def inbox_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-inbox" + ) do |s| + s.polyline(points: "22 12 16 12 14 15 10 15 8 12 2 12") + s.path(d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z") + end + end + + def calendar_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-calendar" + ) do |s| + s.path(d: "M8 2v4") + s.path(d: "M16 2v4") + s.rect(width: "18", height: "18", x: "3", y: "4", rx: "2") + s.path(d: "M3 10h18") + end + end + + def search_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-search" + ) do |s| + s.circle(cx: "11", cy: "11", r: "8") + s.path(d: "M21 21L16.7 16.7") + end + end + + def settings_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-settings" + ) do |s| + s.path(d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73 + l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z") + s.circle(cx: "12", cy: "12", r: "3") + end + end + + def plus_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-plus" + ) do |s| + s.path(d: "M5 12h14") + s.path(d: "M12 5v14") + end + end + + def gallery_vertical_end + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", view_box: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_linejoin: "round", class: "lucide lucide-gallery-vertical-end size-4") do |s| + s.path d: "M7 2h10" + s.path d: "M5 6h14" + s.rect width: "18", height: "12", x: "3", y: "10", rx: "2" + end + end + + def ellipsis_icon + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_line_join: "round", class: "lucide lucide-ellipsis-icon lucide-ellipsis") do |s| + s.circle cx: "12", cy: "12", r: "1" + s.circle cx: "19", cy: "12", r: "1" + s.circle cx: "5", cy: "12", r: "1" + end + end + + def message_circle_question + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_line_join: "round", class: "lucide lucide-message-circle-question-icon lucide-message-circle-question") do |s| + s.path d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z" + s.path d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" + s.path d: "M12 17h.01" + end + end +end diff --git a/lib/ruby_ui/skeleton/skeleton_docs.rb b/docs/app/views/docs/skeleton.rb similarity index 100% rename from lib/ruby_ui/skeleton/skeleton_docs.rb rename to docs/app/views/docs/skeleton.rb diff --git a/lib/ruby_ui/switch/switch_docs.rb b/docs/app/views/docs/switch.rb similarity index 100% rename from lib/ruby_ui/switch/switch_docs.rb rename to docs/app/views/docs/switch.rb diff --git a/lib/ruby_ui/table/table_docs.rb b/docs/app/views/docs/table.rb similarity index 100% rename from lib/ruby_ui/table/table_docs.rb rename to docs/app/views/docs/table.rb diff --git a/lib/ruby_ui/tabs/tabs_docs.rb b/docs/app/views/docs/tabs.rb similarity index 100% rename from lib/ruby_ui/tabs/tabs_docs.rb rename to docs/app/views/docs/tabs.rb diff --git a/lib/ruby_ui/textarea/textarea_docs.rb b/docs/app/views/docs/textarea.rb similarity index 100% rename from lib/ruby_ui/textarea/textarea_docs.rb rename to docs/app/views/docs/textarea.rb diff --git a/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb b/docs/app/views/docs/theme_toggle.rb similarity index 100% rename from lib/ruby_ui/theme_toggle/theme_toggle_docs.rb rename to docs/app/views/docs/theme_toggle.rb diff --git a/lib/ruby_ui/tooltip/tooltip_docs.rb b/docs/app/views/docs/tooltip.rb similarity index 100% rename from lib/ruby_ui/tooltip/tooltip_docs.rb rename to docs/app/views/docs/tooltip.rb diff --git a/lib/ruby_ui/typography/typography_docs.rb b/docs/app/views/docs/typography.rb similarity index 100% rename from lib/ruby_ui/typography/typography_docs.rb rename to docs/app/views/docs/typography.rb diff --git a/docs/app/views/errors/internal_server_error.rb b/docs/app/views/errors/internal_server_error.rb new file mode 100644 index 00000000..b6be81a0 --- /dev/null +++ b/docs/app/views/errors/internal_server_error.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +class Views::Errors::InternalServerError < Views::Base + def view_template + Card(class: "p-8 space-y-6 flex flex-col items-center") do + div(class: "space-y-2") do + Badge(variant: :destructive, class: "font-mono") { "STATUS: 500" } + Heading(level: 1, class: "!leading-tight") { "Oops! Something went wrong" } + Text(class: "text-muted-foreground") { "Something unexpected happened. We're looking into it." } + end + + Link(href: root_path, variant: :primary, class: "w-full") do + house_icon + plain "Go to home" + end + end + end + + private + + def back_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" + ) + end + end + + def house_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" + ) + end + end +end diff --git a/docs/app/views/errors/not_found.rb b/docs/app/views/errors/not_found.rb new file mode 100644 index 00000000..a9de3e2c --- /dev/null +++ b/docs/app/views/errors/not_found.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +class Views::Errors::NotFound < Views::Base + def view_template + Card(class: "p-8 space-y-6 flex flex-col items-center") do + div(class: "space-y-2") do + Badge(variant: :purple, class: "font-mono") { "STATUS: 404" } + Heading(level: 1, class: "!leading-tight") { "Oops! Page not found" } + Text(class: "text-muted-foreground") { "The page you were looking for doesn't exist." } + end + + Link(href: root_path, variant: :primary, class: "w-full") do + house_icon + plain "Go to home" + end + end + end + + private + + def back_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" + ) + end + end + + def house_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" + ) + end + end +end diff --git a/docs/app/views/layouts/application_layout.rb b/docs/app/views/layouts/application_layout.rb new file mode 100644 index 00000000..252d2ec8 --- /dev/null +++ b/docs/app/views/layouts/application_layout.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Views + module Layouts + class ApplicationLayout < Views::Base + include Phlex::Rails::Layout + + def view_template(&block) + doctype + + html do + render Shared::Head.new + + body do + script(defer: true, data_domain: "rubyui.com", src: "https://plausible.io/js/script.js") + render Shared::Navbar.new + main(&block) + render Shared::Footer.new + render Shared::Flashes.new(notice: flash[:notice], alert: flash[:alert]) + end + end + end + end + end +end diff --git a/docs/app/views/layouts/docs_layout.rb b/docs/app/views/layouts/docs_layout.rb new file mode 100644 index 00000000..85fe4adc --- /dev/null +++ b/docs/app/views/layouts/docs_layout.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Views + module Layouts + class DocsLayout < Views::Base + include Phlex::Rails::Layout + + def view_template(&block) + doctype + + html do + render Shared::Head.new + + body do + render Shared::Navbar.new + div(class: "flex-1") do + div(class: "border-b") do + div(class: "container px-4 flex-1 items-start md:grid md:grid-cols-[220px_minmax(0,1fr)] md:gap-6 lg:grid-cols-[240px_minmax(0,1fr)] lg:gap-10") do + render Shared::Sidebar.new + main(class: "relative py-6 lg:gap-10 lg:py-8 xl:grid xl:grid-cols-[1fr_300px]", &block) + end + end + end + render Shared::Footer.new + render Shared::Flashes.new(notice: flash[:notice], alert: flash[:alert]) + end + end + end + end + end +end diff --git a/docs/app/views/layouts/errors_layout.rb b/docs/app/views/layouts/errors_layout.rb new file mode 100644 index 00000000..978841cd --- /dev/null +++ b/docs/app/views/layouts/errors_layout.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Views + module Layouts + class ErrorsLayout < Views::Base + include Phlex::Rails::Layout + + def view_template(&block) + doctype + + html do + render Shared::Head.new + + body do + main(class: "relative flex flex-col items-center justify-center gap-y-6 h-screen w-screen overflow-y-scroll") do + render Shared::GridPattern.new + ThemeToggle(class: "hidden") # In order for dark mode to work, we need to render the theme toggle somewhere in the DOM + render Shared::Logo.new + div(class: "container w-full max-w-md", &block) + end + render Shared::Flashes.new(notice: flash[:notice], alert: flash[:alert]) + end + end + end + end + end +end diff --git a/docs/app/views/layouts/examples_layout.rb b/docs/app/views/layouts/examples_layout.rb new file mode 100644 index 00000000..e79a2dd9 --- /dev/null +++ b/docs/app/views/layouts/examples_layout.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Views + module Layouts + class ExamplesLayout < Views::Base + include Phlex::Rails::Layout + + def view_template(&block) + doctype + + html do + render Shared::Head.new + + body do + block.call + render Shared::Flashes.new(notice: flash[:notice], alert: flash[:alert]) + end + end + end + end + end +end diff --git a/docs/app/views/layouts/mailer_layout.rb b/docs/app/views/layouts/mailer_layout.rb new file mode 100644 index 00000000..79d4c770 --- /dev/null +++ b/docs/app/views/layouts/mailer_layout.rb @@ -0,0 +1,330 @@ +module Views + module Layouts + class MailerLayout < Views::Base + include Phlex::Rails::Layout + + def view_template(&block) + html do + head do + meta(name: "viewport", content: "width=device-width, initial-scale=1.0") + meta(http_equiv: "Content-Type", content: "text/html; charset=UTF-8") + title { "Simple Transactional Email" } + style(media: "all", type: "text/css") do + <<-CSS + :root { + --font: Helvetica, sans-serif; + --background: hsl(0, 0%, 100%); + --foreground: hsl(0, 0%, 3.9%); + --primary: hsl(0, 0%, 9%); + --primary-foreground: hsl(0, 0%, 98%); + --muted: hsl(0, 0%, 96.1%); + --muted-foreground: hsl(0, 0%, 45.1%); + --border: hsl(0, 0%, 89.8%); + --radius: 0.4rem; + } + body { + font-family: var(--font); + -webkit-font-smoothing: antialiased; + font-size: 16px; + line-height: 1.3; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + } + table { + border-collapse: separate; + mso-table-lspace: 0; + mso-table-rspace: 0; + width: 100%; + } + table td { + font-family: var(--font); + font-size: 16px; + vertical-align: top; + } + body { + background-color: var(--muted); + margin: 0; + padding: 0 4px; + } + .body { + background-color: var(--muted); + width: 100%; + } + .container { + margin: 0 auto !important; + max-width: 600px; + padding: 0; + padding-top: 24px; + width: 600px; + } + .content { + box-sizing: border-box; + display: block; + margin: 0 auto; + max-width: 600px; + padding: 0; + } + .main-wrapper { + border-radius: calc(var(--radius) * 2); + overflow: hidden; + border: 1px solid var(--border); + background: var(--background); + } + .main { + width: 100%; + } + .wrapper { + box-sizing: border-box; + padding: 24px; + } + .footer { + clear: both; + padding-top: 24px; + text-align: center; + width: 100%; + } + .footer td, .footer p, .footer span, .footer a { + color: var(--muted-foreground); + font-size: 16px; + text-align: center; + } + p { + font-family: var(--font); + color: var(--muted-foreground); + font-size: 16px; + font-weight: normal; + margin: 0; + margin-bottom: 16px; + line-height: 1.7; + } + strong { + font-weight: 600; + color: var(--foreground); + } + h2 { + font-size: 20px; + letter-spacing: -0.5px; + } + a { + color: var(--primary); + text-decoration: underline; + } + .btn { + box-sizing: border-box; + margin-bottom: 16px; + } + .btn > tbody > tr > td { + padding-bottom: 16px; + } + .btn table { + width: auto; + } + .btn table td { + background-color: var(--background); + border-radius: var(--radius); + text-align: center; + } + .btn a { + background-color: var(--background); + border: solid 2px var(--primary); + border-radius: var(--radius); + box-sizing: border-box; + color: var(--primary-foreground); + cursor: pointer; + display: inline-block; + font-size: 16px; + font-weight: 600; + margin: 0; + padding: 8px 24px; + text-decoration: none; + text-transform: capitalize; + } + .btn-primary table td { + background-color: var(--primary); + } + .btn-primary a { + background-color: var(--primary); + border-color: var(--primary); + color: var(--primary-foreground); + } + @media all { + .btn-primary table td:hover { + opacity: 0.9 !important; + } + } + .btn-primary a:hover { + opacity: 0.9 !important; + } + @media { + .last { + margin-bottom: 0; + } + } + .first { + margin-top: 0; + } + .align-center { + text-align: center; + } + .align-right { + text-align: right; + } + .align-left { + text-align: left; + } + .text-link { + color: var(--primary) !important; + text-decoration: underline !important; + } + .clear { + clear: both; + } + .mt0 { + margin-top: 0; + } + .mb0 { + margin-bottom: 0; + } + .preheader { + color: transparent; + display: none; + height: 0; + max-height: 0; + max-width: 0; + opacity: 0; + overflow: hidden; + mso-hide: all; + visibility: hidden; + width: 0; + } + .powered-by a { + text-decoration: none; + } + .logo-container { + text-align: center; + padding: 20px 10px; + } + @media only screen and (max-width:640px) { + .main p, .main td, .main span { + font-size: 16px !important; + } + } + .main { + border-radius: var(--radius) * 4; + } + .wrapper { + padding: 30px !important; + } + .content { + padding: 0 !important; + } + .container { + padding: 0 !important; + padding-top: 8px !important; + width: 100% !important; + } + .btn table { + max-width: 100% !important; + } + .btn a { + font-size: 16px !important; + max-width: 100% !important; + width: 100% !important; + } + @media all { + .ExternalClass { + width: 100%; + } + } + .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { + line-height: 100%; + } + .apple-link a { + color: inherit !important; + font-family: inherit !important; + font-size: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + text-decoration: none !important; + } + #MessageViewBody a { + color: inherit; + text-decoration: none; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + line-height: inherit; + } + CSS + end + end + body do + table( + role: "presentation", + border: "0", + cellpadding: "0", + cellspacing: "0", + class: "body" + ) do + tr do + td { " " } + td(class: "container") do + div(class: "content") do + comment { "START CENTERED WHITE CONTAINER" } + # span(class: "preheader") do + # "This is preheader text. Some clients will show this text as a preview." + # end + + div(class: "logo-container") do + a(href: ENV["HOST"]) do + img(src: image_url("logo.svg"), alt: ENV["APP_NAME"], height: "20") + end + end + + div(class: "main-wrapper") do + table( + role: "presentation", + border: "0", + cellpadding: "0", + cellspacing: "0", + class: "main" + ) do + comment { "START MAIN CONTENT AREA" } + tr do + td(class: "wrapper") do + yield + end + end + comment { "END MAIN CONTENT AREA" } + end + end + comment { "START FOOTER" } + div(class: "footer") do + table( + role: "presentation", + border: "0", + cellpadding: "0", + cellspacing: "0" + ) do + tr do + td(class: "content-block") do + span(class: "apple-link") do + "#{ENV["APP_NAME"]}, #{ENV["COMPANY_ADDRESS"]}" + end + end + end + end + end + comment { "END FOOTER" } + comment { "END CENTERED WHITE CONTAINER" } + end + end + td { " " } + end + end + end + end + end + end + end +end diff --git a/docs/app/views/layouts/pages_layout.rb b/docs/app/views/layouts/pages_layout.rb new file mode 100644 index 00000000..9bd8c1ec --- /dev/null +++ b/docs/app/views/layouts/pages_layout.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Views + module Layouts + class PagesLayout < Views::Base + include Phlex::Rails::Layout + + def view_template(&block) + doctype + + html do + render Shared::Head.new + + body do + render Shared::Navbar.new + main(class: "relative", &block) + render Shared::Footer.new + render Shared::Flashes.new(notice: flash[:notice], alert: flash[:alert]) + end + end + end + end + end +end diff --git a/docs/app/views/mailers/base_mailer.rb b/docs/app/views/mailers/base_mailer.rb new file mode 100644 index 00000000..4b5841e5 --- /dev/null +++ b/docs/app/views/mailers/base_mailer.rb @@ -0,0 +1,51 @@ +# Provides helper method for rendering CTA links + +module Views + module Mailers + class BaseMailer < Views::Base + private + + def para(&) + p(&) # See mailer_layout.rb to change styles + end + + def subheader(&) + h2(&) # See mailer_layout.rb to change styles + end + + def cta(href:, &block) + table( + role: "presentation", + border: "0", + cellpadding: "0", + cellspacing: "0", + class: "btn btn-primary" + ) do + tbody do + tr do + td(align: "left") do + table( + role: "presentation", + border: "0", + cellpadding: "0", + cellspacing: "0" + ) do + tbody do + tr do + td do + a( + href: href, + target: "_blank" + ) { block.call } + end + end + end + end + end + end + end + end + end + end + end +end diff --git a/docs/app/views/pages/home.rb b/docs/app/views/pages/home.rb new file mode 100644 index 00000000..88deffb8 --- /dev/null +++ b/docs/app/views/pages/home.rb @@ -0,0 +1,288 @@ +# frozen_string_literal: true + +class Views::Pages::Home < Views::Base + def view_template + # Hero Section + section(class: "space-y-6 pt-6 md:pt-10 lg:pt-16") do + div(class: "container flex max-w-[64rem] flex-col items-center gap-4 text-center mx-auto") do + Link(href: docs_changelog_path, variant: :outline, class: "rounded-2xl px-4 py-1.5 text-sm font-medium") do + span(class: "sm:hidden") { "New components available" } + span(class: "hidden sm:inline") { "Combobox updates and more" } + svg(xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewbox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", stroke_linecap: "round", stroke_linejoin: "round", class: "ml-2 h-4 w-4") { |s| + s.path(d: "M5 12h14") + s.path(d: "m12 5 7 7-7 7") + } + end + h1(class: "leading-tighter text-3xl font-semibold tracking-tight text-balance text-primary lg:leading-[1.1] lg:font-semibold xl:text-5xl xl:tracking-tighter max-w-4xl") do + [ + "Build sharp Ruby interfaces.", + "Reusable UI components for Ruby developers.", + "Pure Ruby UI, built with Phlex.", + "Elevate your Rails apps with RubyUI.", + "The sharpest way to build in Ruby." + ].sample + end + p(class: "max-w-4xl text-base text-balance text-foreground sm:text-lg") do + "A UI component library, crafted precisely for Ruby devs who want to stay organised and build modern apps, fast." + end + div(class: "space-x-4 mt-4") do + Link(href: docs_introduction_path, variant: :primary, size: :lg) { "Documentation" } + Link(href: docs_components_path, variant: :outline, size: :lg) { "View Components" } + end + end + end + + # Components Mosaic Section + section(class: "container py-8 md:py-12 lg:py-24 mx-auto max-w-6xl") do + div(class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6") do + # COLUMN 1 + div(class: "space-y-6") do + # PAYMENT CARD + Card do + CardHeader(class: "space-y-1") do + CardTitle { "Payment Method" } + CardDescription { "All transactions are secure and encrypted." } + end + CardContent(class: "grid gap-4") do + div(class: "grid gap-2") do + Label { "Name on Card" } + Input(placeholder: "John Doe") + end + div(class: "grid gap-2") do + Label { "Card Number" } + Input(placeholder: "1234 5678 9012 3456") + end + div(class: "grid grid-cols-3 gap-4") do + div(class: "grid gap-2 col-span-2") do + Label { "Expires" } + div(class: "grid grid-cols-2 gap-2") do + Button(variant: :outline, class: "justify-between text-muted-foreground font-normal") do + span { "Month" } + lucide_icon "chevron-down", class: "h-3 w-3 opacity-50" + end + Button(variant: :outline, class: "justify-between text-muted-foreground font-normal") do + span { "Year" } + lucide_icon "chevron-down", class: "h-3 w-3 opacity-50" + end + end + end + div(class: "grid gap-2") do + Label { "CVV" } + Input(placeholder: "CVV") + end + end + div(class: "flex items-center space-x-2 pt-2") do + Checkbox(id: "shipping") + Label(for: "shipping", class: "text-sm font-normal") { "Same as shipping address" } + end + end + CardFooter(class: "flex justify-between gap-2") do + Button(variant: :outline, class: "flex-1") { "Cancel" } + Button(class: "flex-1") { "Pay Now" } + end + end + + # ALERT Showcase + Alert do + lucide_icon "terminal", class: "h-4 w-4" + AlertTitle { "Heads up!" } + AlertDescription { "You can add components directly to your app using Phlex." } + end + + # ACTIVITY FEED + Card do + CardHeader(class: "pb-3") do + CardTitle { "Activity Feed" } + end + CardContent(class: "flex flex-wrap gap-2") do + Badge(variant: :sky) { "In Review" } + Badge(variant: :success) { "Ready to Ship" } + Badge(variant: :outline) { "Draft" } + Badge(variant: :destructive) { "Rejected" } + Badge(variant: :primary) { "Deployed" } + Badge(variant: :amber) { "Agent Thinking" } + end + end + end + + # COLUMN 2 + div(class: "space-y-6") do + # TABS + Tabs(default_value: "account") do + TabsList(class: "grid w-full grid-cols-2") do + TabsTrigger(value: "account") { "Account" } + TabsTrigger(value: "password") { "Password" } + end + TabsContent(value: "account") do + Card do + CardHeader do + CardTitle { "Account" } + CardDescription { "Make changes to your account here." } + end + CardContent(class: "space-y-2") do + div(class: "space-y-1") do + Label { "Username" } + Input(placeholder: "@djalma") + end + div(class: "space-y-1") do + Label { "Email" } + Input(placeholder: "djalma@nossomos.cc") + end + end + CardFooter do + Button(size: :sm, class: "w-full") { "Save changes" } + end + end + end + end + + # TEAM MEMBERS + Card do + CardHeader do + CardTitle { "Team Members" } + CardDescription { "Collaborate with your team." } + end + CardContent(class: "space-y-6") do + team_member("Sofia Davis", "@sdavis", "https://i.pravatar.cc/150?u=sofia") + team_member("Jackson Lee", "@jlee", "https://i.pravatar.cc/150?u=jackson") + team_member("Djalma Araújo", "@djalma", "https://i.pravatar.cc/150?u=djalma") + team_member("George Kettle", "@gkettle", "https://i.pravatar.cc/150?u=george") + end + CardFooter do + Button(variant: :outline, class: "w-full") { "Invite Members" } + end + end + end + + # COLUMN 3 + div(class: "space-y-6") do + # PROGRESS / QUOTA + Card do + CardHeader(class: "pb-4") do + div(class: "flex items-center justify-between") do + CardTitle { "Storage Usage" } + span(class: "text-xs text-muted-foreground") { "12.4GB / 20GB" } + end + end + CardContent do + Progress(value: 62) + end + CardFooter do + Button(variant: :ghost, size: :sm, class: "w-full text-xs") { "Upgrade Plan" } + end + end + + # SETTINGS / SWITCHES + Card do + CardHeader do + CardTitle { "Settings" } + CardDescription { "Manage your preferences." } + end + CardContent(class: "space-y-4") do + div(class: "flex items-center justify-between rounded-lg border p-4 shadow-sm") do + div(class: "space-y-0.5") do + p(class: "font-medium text-sm") { "Kubernetes" } + p(class: "text-xs text-muted-foreground") { "Highly available cluster." } + end + Switch(checked: true) + end + div(class: "flex items-center justify-between rounded-lg border p-4 shadow-sm") do + div(class: "space-y-0.5") do + p(class: "font-medium text-sm") { "Dark Mode" } + p(class: "text-xs text-muted-foreground") { "Use the dark theme." } + end + Switch() + end + end + end + + # AI AGENT CHAT + Card do + CardHeader(class: "pb-2") do + div(class: "flex items-center justify-between") do + CardTitle { "AI Assistant" } + Badge(variant: :secondary, size: :sm) { "v4.0" } + end + end + CardContent(class: "space-y-4") do + div(class: "max-w-[80%] rounded-lg bg-muted p-3 text-sm") do + "How can I help you build with Ruby today?" + end + div(class: "flex flex-wrap gap-2") do + Button(variant: :outline, size: :sm, class: "h-7 text-[10px] gap-1") do + lucide_icon "plus", class: "h-3 w-3" + span { "Add Context" } + end + Button(variant: :outline, size: :sm, class: "h-7 text-[10px] gap-1") do + lucide_icon "globe", class: "h-3 w-3" + span { "Web Search" } + end + end + end + CardFooter(class: "flex flex-col gap-3 pt-0") do + div(class: "flex items-center w-full gap-2") do + Button(variant: :ghost, size: :icon, class: "h-8 w-8 shrink-0") do + lucide_icon "plus-circle", class: "h-4 w-4" + end + div(class: "relative flex-1") do + Input(placeholder: "Ask anything...", class: "pr-10 h-10") + Button(variant: :primary, size: :icon, class: "absolute right-1 top-1 h-8 w-8") do + lucide_icon "arrow-up", class: "h-4 w-4" + end + end + end + div(class: "flex items-center gap-2 text-[10px] text-muted-foreground") do + lucide_icon "zap", class: "h-3 w-3" + span { "GPT-4o" } + span(class: "mx-1") { "•" } + lucide_icon "layers", class: "h-3 w-3" + span { "Professional Plan" } + end + end + end + end + end + end + end + + private + + def Label(class: nil, **attrs, &block) + base_classes = "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" + + label(class: [base_classes, binding.local_variable_get(:class)], **attrs, &block) + end + + def team_member(name, handle, avatar_url) + div(class: "flex items-center justify-between") do + div(class: "flex items-center gap-4") do + Avatar do + AvatarImage(src: avatar_url, alt: name) + AvatarFallback { name.split.map(&:first).join } + end + div do + p(class: "text-sm font-medium leading-none") { name } + p(class: "text-xs text-muted-foreground") { handle } + end + end + Button(variant: :ghost, size: :sm) { "Edit" } + end + end + + def activity_item(title, time, status) + div(class: "flex items-center gap-4") do + div(class: [ + "h-2 w-2 rounded-full", + (if status == :success + "bg-green-500" + else + ((status == :warning) ? "bg-amber-500" : "bg-blue-500") + end) + ]) + div do + p(class: "text-sm font-medium") { title } + p(class: "text-xs text-muted-foreground") { time } + end + end + end +end diff --git a/docs/app/views/themes/show.rb b/docs/app/views/themes/show.rb new file mode 100644 index 00000000..e653ebf3 --- /dev/null +++ b/docs/app/views/themes/show.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class Views::Themes::Show < Views::Base + def initialize(theme:) + @theme = theme + end + + def view_template + render Shared::Container.new(size: "2xl", class: "py-12") do + div(class: "md:flex items-center justify-between") do + div do + Heading(level: 1) { "Themes" } + Text(as: "p", size: "5", weight: "medium") { "Customize your app fast with hand-picked themes. Compatible with shadcn/ui." } + end + div(class: "flex gap-x-2 mt-4") do + render Themes::CustomizePopover.new(theme: @theme) + render Themes::CopyCode.new(theme: @theme) + end + end + + div(class: "flex flex-wrap justify-between -mx-2 pt-12") do + div(class: "flex flex-col gap-y-6 p-3 w-full sm:w-1/2 xl:w-1/3") do + render Themes::Grid::RepoTabs.new + render Themes::Grid::LineGraph.new + render Themes::Grid::Card.new + end + div(class: "flex flex-col gap-y-6 p-3 w-full sm:w-1/2 xl:w-1/3") do + render Themes::Grid::Chat.new + render Themes::Grid::Chart.new + render Themes::Grid::CreateEvent.new + end + div(class: "flex flex-col gap-y-6 p-3 w-full sm:w-1/2 xl:w-1/3") do + # render Themes::Grid::Command.new + render Themes::Grid::Signin.new + # render Themes::Grid::Table.new + end + end + end + end +end diff --git a/docs/bin/bundle b/docs/bin/bundle new file mode 100755 index 00000000..42c7fd7c --- /dev/null +++ b/docs/bin/bundle @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/docs/bin/ci b/docs/bin/ci new file mode 100755 index 00000000..4137ad5b --- /dev/null +++ b/docs/bin/ci @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "active_support/continuous_integration" + +CI = ActiveSupport::ContinuousIntegration +require_relative "../config/ci.rb" diff --git a/docs/bin/dev b/docs/bin/dev new file mode 100755 index 00000000..58c60fae --- /dev/null +++ b/docs/bin/dev @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# Default to port 3000 if not specified +export PORT="${PORT:-3000}" + +if command -v overmind &>/dev/null; then + exec overmind start -f Procfile.dev "$@" +else + if ! gem list foreman -i --silent; then + echo "Installing foreman..." + gem install foreman + fi + foreman start -f Procfile.dev "$@" +fi diff --git a/docs/bin/docker-entrypoint b/docs/bin/docker-entrypoint new file mode 100755 index 00000000..57567d69 --- /dev/null +++ b/docs/bin/docker-entrypoint @@ -0,0 +1,14 @@ +#!/bin/bash -e + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ]; then + LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) + export LD_PRELOAD +fi + +# If running the rails server then create or migrate existing database +if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/docs/bin/rails b/docs/bin/rails new file mode 100755 index 00000000..efc03774 --- /dev/null +++ b/docs/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/docs/bin/rake b/docs/bin/rake new file mode 100755 index 00000000..4fbf10b9 --- /dev/null +++ b/docs/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/docs/bin/rubocop b/docs/bin/rubocop new file mode 100755 index 00000000..5a205047 --- /dev/null +++ b/docs/bin/rubocop @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +# Explicit RuboCop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + +load Gem.bin_path("rubocop", "rubocop") diff --git a/docs/bin/setup b/docs/bin/setup new file mode 100755 index 00000000..a4c310d0 --- /dev/null +++ b/docs/bin/setup @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby +require "fileutils" + +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system("bundle check") || system!("bundle install") + system!("pnpm install --force") + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + system! "bin/rails db:reset" if ARGV.include?("--reset") + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end +end diff --git a/docs/config.ru b/docs/config.ru new file mode 100644 index 00000000..4a3c09a6 --- /dev/null +++ b/docs/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/docs/config/.editorconfig b/docs/config/.editorconfig new file mode 100644 index 00000000..e3207ea2 --- /dev/null +++ b/docs/config/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/docs/config/application.rb b/docs/config/application.rb new file mode 100644 index 00000000..b0f97a18 --- /dev/null +++ b/docs/config/application.rb @@ -0,0 +1,29 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module RubyUiWeb + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.1 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + + config.exceptions_app = ->(env) { ErrorsController.action(:not_found).call(env) } + end +end diff --git a/docs/config/boot.rb b/docs/config/boot.rb new file mode 100644 index 00000000..988a5ddc --- /dev/null +++ b/docs/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/docs/config/cable.yml b/docs/config/cable.yml new file mode 100644 index 00000000..b0c4fba3 --- /dev/null +++ b/docs/config/cable.yml @@ -0,0 +1,11 @@ +development: + adapter: redis + url: redis://localhost:6379/1 + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: phlex_components_library_production diff --git a/docs/config/ci.rb b/docs/config/ci.rb new file mode 100644 index 00000000..930d383c --- /dev/null +++ b/docs/config/ci.rb @@ -0,0 +1,21 @@ +# Run using bin/ci + +CI.run do + step "Setup", "bin/setup --skip-server" + + step "Style: Ruby", "bin/rubocop" + + step "Security: Importmap vulnerability audit", "bin/importmap audit" + + step "Tests: Rails", "bin/rails test" + step "Tests: System", "bin/rails test:system" + step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant" + + # Optional: set a green GitHub commit status to unblock PR merge. + # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`. + # if success? + # step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" + # else + # failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again." + # end +end diff --git a/docs/config/credentials/production.yml.enc b/docs/config/credentials/production.yml.enc new file mode 100644 index 00000000..7651e988 --- /dev/null +++ b/docs/config/credentials/production.yml.enc @@ -0,0 +1 @@ +V8SNuyxE0Iq+Yq161KEN/j3hjKfRN68Q81rARXyEOeFKNPw7o/SktsjnFSg9jDgZ4692i8RZiz5h63ZcLe12+6Pd6lA2gNOYv8aGUdl686JnDDL92PLC3ZOMfZx1JGD0G33SZumqxrVgpMLgpY5FKLEGy09jNHEdD9mYv3siJKU89xd0pdKTCoIbpcVXfEfuPyuul8hRi8VJjPLoIeQnXTSNNVNlvhVqFAEDIhPoilhqbxSlK8uSJPfAlUCzeU3A5x1haKDR3grU324jWkQX5fSO7JRXc2MqJxVNu/npX6CWaXH/NTVmbPi1YmcpI8LbROzfDLHx5ejH1KocKgIGbJss6Mo9zZZHD1xY4IiEQT+AofOjpwapnFhFvcuNMZImTaczxICVcJdM+EffipFUNuA2QGMX--SXsZ0psbmaVD96/1--lU3yc2xi8ZE/Mgo31zKzKA== \ No newline at end of file diff --git a/docs/config/database.yml b/docs/config/database.yml new file mode 100644 index 00000000..427959d1 --- /dev/null +++ b/docs/config/database.yml @@ -0,0 +1,82 @@ +# PostgreSQL. Versions 9.3 and up are supported. +# +# Install the pg driver: +# gem install pg +# On macOS with Homebrew: +# gem install pg -- --with-pg-config=/usr/local/bin/pg_config +# On macOS with MacPorts: +# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config +# On Windows: +# gem install pg +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +# +# Configure Using Gemfile +# gem "pg" +# +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: storage/development.sqlite3 + + # The specified database role being used to connect to postgres. + # To create additional roles in postgres see `$ createuser --help`. + # When left blank, postgres will use the default role. This is + # the same name as the operating system user running Rails. + #username: phlex_components_library + + # The password associated with the postgres role (username). + #password: + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + #host: localhost + + # The TCP port the server listens on. Defaults to 5432. + # If your server runs on a different port number, change accordingly. + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # Defaults to warning. + #min_messages: notice + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: storage/test.sqlite3 + +# As with config/credentials.yml, you never want to store sensitive information, +# like your database password, in your source code. If your source code is +# ever seen by anyone, they now have access to your database. +# +# Instead, provide the password or a full connection URL as an environment +# variable when you boot the app. For example: +# +# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" +# +# If the connection URL is provided in the special DATABASE_URL environment +# variable, Rails will automatically merge its configuration values on top of +# the values provided in this file. Alternatively, you can specify a connection +# URL environment variable explicitly: +# +# production: +# url: <%= ENV["MY_APP_DATABASE_URL"] %> +# +# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database +# for a full overview on how database connection configuration can be specified. +# +production: + <<: *default + database: storage/production.sqlite3 diff --git a/docs/config/dockerfile.yml b/docs/config/dockerfile.yml new file mode 100644 index 00000000..f96c3fc4 --- /dev/null +++ b/docs/config/dockerfile.yml @@ -0,0 +1,7 @@ +# generated by dockerfile-rails + +--- +options: + fullstaq: true + label: + fly_launch_runtime: rails diff --git a/docs/config/environment.rb b/docs/config/environment.rb new file mode 100644 index 00000000..cac53157 --- /dev/null +++ b/docs/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/docs/config/environments/development.rb b/docs/config/environments/development.rb new file mode 100644 index 00000000..bad4e910 --- /dev/null +++ b/docs/config/environments/development.rb @@ -0,0 +1,78 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Make code changes take effect immediately without server restart. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing. + config.server_timing = true + + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + config.public_file_server.headers = {"cache-control" => "public, max-age=#{2.days.to_i}"} + else + config.action_controller.perform_caching = false + end + + # Change to :null_store to avoid any caching. + config.cache_store = :memory_store + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + # Make template changes take effect immediately. + config.action_mailer.perform_caching = false + + # Set localhost to be used by links generated in mailer templates. + config.action_mailer.default_url_options = {host: "localhost", port: 3000} + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Append comments with runtime information tags to SQL queries in logs. + config.active_record.query_log_tags_enabled = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Highlight code that triggered redirect in logs. + config.action_dispatch.verbose_redirect_logs = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + # config.generators.apply_rubocop_autocorrect_after_generate! +end diff --git a/docs/config/environments/production.rb b/docs/config/environments/production.rb new file mode 100644 index 00000000..72e4dacd --- /dev/null +++ b/docs/config/environments/production.rb @@ -0,0 +1,89 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). + config.eager_load = true + + # Full error reports are disabled. + config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. + config.action_controller.perform_caching = true + + # Cache assets for far-future expiry since they are all digest stamped. + config.public_file_server.headers = {"cache-control" => "public, max-age=#{1.year.to_i}"} + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + # Log to STDOUT with the current request id as a default log tag. + config.log_tags = [:request_id] + config.logger = ActiveSupport::TaggedLogging.logger($stdout) + + # Change to "debug" to log everything (including potentially personally-identifiable information!). + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = "/up" + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Replace the default in-process memory cache store with a durable alternative. + # config.cache_store = :mem_cache_store + + # Replace the default in-process and non-durable queuing backend for Active Job. + # config.active_job.queue_adapter = :resque + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = {host: "example.com"} + + # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit. + # config.action_mailer.smtp_settings = { + # user_name: Rails.application.credentials.dig(:smtp, :user_name), + # password: Rails.application.credentials.dig(:smtp, :password), + # address: "smtp.example.com", + # port: 587, + # authentication: :plain + # } + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [:id] + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/docs/config/environments/test.rb b/docs/config/environments/test.rb new file mode 100644 index 00000000..0fb6d30c --- /dev/null +++ b/docs/config/environments/test.rb @@ -0,0 +1,53 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = {"cache-control" => "public, max-age=3600"} + + # Show full error reports. + config.consider_all_requests_local = true + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = {host: "example.com"} + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/docs/config/initializers/assets.rb b/docs/config/initializers/assets.rb new file mode 100644 index 00000000..48732442 --- /dev/null +++ b/docs/config/initializers/assets.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = "1.0" + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path diff --git a/docs/config/initializers/content_security_policy.rb b/docs/config/initializers/content_security_policy.rb new file mode 100644 index 00000000..d51d7139 --- /dev/null +++ b/docs/config/initializers/content_security_policy.rb @@ -0,0 +1,29 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag` +# # if the corresponding directives are specified in `content_security_policy_nonce_directives`. +# # config.content_security_policy_nonce_auto = true +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/docs/config/initializers/filter_parameter_logging.rb b/docs/config/initializers/filter_parameter_logging.rb new file mode 100644 index 00000000..c0b717f7 --- /dev/null +++ b/docs/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc +] diff --git a/docs/config/initializers/inflections.rb b/docs/config/initializers/inflections.rb new file mode 100644 index 00000000..ddf7cf8e --- /dev/null +++ b/docs/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.acronym "CLI" +end diff --git a/docs/config/initializers/lookbook.rb b/docs/config/initializers/lookbook.rb new file mode 100644 index 00000000..8e9b8f90 --- /dev/null +++ b/docs/config/initializers/lookbook.rb @@ -0,0 +1 @@ +# frozen_string_literal: true diff --git a/docs/config/initializers/permissions_policy.rb b/docs/config/initializers/permissions_policy.rb new file mode 100644 index 00000000..00f64d71 --- /dev/null +++ b/docs/config/initializers/permissions_policy.rb @@ -0,0 +1,11 @@ +# Define an application-wide HTTP permissions policy. For further +# information see https://developers.google.com/web/updates/2018/06/feature-policy +# +# Rails.application.config.permissions_policy do |f| +# f.camera :none +# f.gyroscope :none +# f.microphone :none +# f.usb :none +# f.fullscreen :self +# f.payment :self, "https://secure.example.com" +# end diff --git a/docs/config/initializers/phlex.rb b/docs/config/initializers/phlex.rb new file mode 100644 index 00000000..13fd15ae --- /dev/null +++ b/docs/config/initializers/phlex.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Views +end + +module Components + extend Phlex::Kit +end + +Rails.autoloaders.main.push_dir( + "#{Rails.root}/app/views", namespace: Views +) + +Rails.autoloaders.main.push_dir( + "#{Rails.root}/app/components", namespace: Components +) diff --git a/docs/config/initializers/ruby_ui.rb b/docs/config/initializers/ruby_ui.rb new file mode 100644 index 00000000..5d89e09d --- /dev/null +++ b/docs/config/initializers/ruby_ui.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "ruby_ui" + +module RubyUI + extend Phlex::Kit +end + +# Allow using RubyUI instead RubyUi +Rails.autoloaders.main.inflector.inflect( + "ruby_ui" => "RubyUI" +) + +# Autoload RubyUI components directly from the gem's lib/ruby_ui directory +# (the gem lives at ../gem in this monorepo, resolved via Bundler). +gem_ruby_ui_path = "#{Gem.loaded_specs["ruby_ui"].gem_dir}/lib/ruby_ui" + +Rails.autoloaders.main.push_dir(gem_ruby_ui_path, namespace: RubyUI) +Rails.autoloaders.main.collapse( + Dir.glob("#{gem_ruby_ui_path}/*").select { |p| File.directory?(p) && File.basename(p) != "docs" } +) + +# The gem ships *_docs.rb files (Views::Docs::*) and a docs/ scaffolding folder +# (Docs::*). The Rails docs app has its own richer implementations under +# app/views/docs and app/components/docs, so we ignore the gem versions to +# avoid Zeitwerk namespace mismatches. +Rails.autoloaders.main.ignore("#{gem_ruby_ui_path}/docs") +Dir.glob("#{gem_ruby_ui_path}/**/*_docs.rb").each do |f| + Rails.autoloaders.main.ignore(f) +end diff --git a/docs/config/locales/en.yml b/docs/config/locales/en.yml new file mode 100644 index 00000000..8ca56fc7 --- /dev/null +++ b/docs/config/locales/en.yml @@ -0,0 +1,33 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# "true": "foo" +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/docs/config/puma.rb b/docs/config/puma.rb new file mode 100644 index 00000000..38c4b865 --- /dev/null +++ b/docs/config/puma.rb @@ -0,0 +1,42 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. You can set it to `auto` to automatically start a worker +# for each available processor. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Run the Solid Queue supervisor inside of Puma for single-server deployments. +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/docs/config/routes.rb b/docs/config/routes.rb new file mode 100644 index 00000000..bee40040 --- /dev/null +++ b/docs/config/routes.rb @@ -0,0 +1,73 @@ +Rails.application.routes.draw do + get "themes/:theme", to: "themes#show", as: :theme + + scope "docs" do + # GETTING STARTED + get "introduction", to: "docs#introduction", as: :docs_introduction + get "installation", to: "docs#installation", as: :docs_installation + get "theming", to: "docs#theming", as: :docs_theming + get "dark_mode", to: "docs#dark_mode", as: :docs_dark_mode + get "customizing_components", to: "docs#customizing_components", as: :docs_customizing_components + + # INSTALLATION + get "installation/rails_bundler", to: "docs#installation_rails_bundler", as: :docs_installation_rails_bundler + get "installation/rails_importmaps", to: "docs#installation_rails_importmaps", as: :docs_installation_rails_importmaps + + # COMPONENTS + get "components", to: "docs#components", as: :docs_components + get "changelog", to: "docs#changelog", as: :docs_changelog + get "accordion", to: "docs#accordion", as: :docs_accordion + get "alert", to: "docs#alert_component", as: :docs_alert # alert is a reserved word for controller action + get "alert_dialog", to: "docs#alert_dialog", as: :docs_alert_dialog + get "aspect_ratio", to: "docs#aspect_ratio", as: :docs_aspect_ratio + get "avatar", to: "docs#avatar", as: :docs_avatar + get "badge", to: "docs#badge", as: :docs_badge + get "breadcrumb", to: "docs#breadcrumb", as: :docs_breadcrumb + get "button", to: "docs#button", as: :docs_button + get "card", to: "docs#card", as: :docs_card + get "carousel", to: "docs#carousel", as: :docs_carousel + get "calendar", to: "docs#calendar", as: :docs_calendar + get "chart", to: "docs#chart", as: :docs_chart + get "checkbox", to: "docs#checkbox", as: :docs_checkbox + get "checkbox_group", to: "docs#checkbox_group", as: :docs_checkbox_group + get "clipboard", to: "docs#clipboard", as: :docs_clipboard + get "codeblock", to: "docs#codeblock", as: :docs_codeblock + get "collapsible", to: "docs#collapsible", as: :docs_collapsible + get "combobox", to: "docs#combobox", as: :docs_combobox + get "command", to: "docs#command", as: :docs_command + get "context_menu", to: "docs#context_menu", as: :docs_context_menu + get "date_picker", to: "docs#date_picker", as: :docs_date_picker + get "dialog", to: "docs#dialog", as: :docs_dialog + get "dropdown_menu", to: "docs#dropdown_menu", as: :docs_dropdown_menu + get "form", to: "docs#form", as: :docs_form + get "hover_card", to: "docs#hover_card", as: :docs_hover_card + get "input", to: "docs#input", as: :docs_input + get "link", to: "docs#link", as: :docs_link + get "masked_input", to: "docs#masked_input", as: :masked_input + get "pagination", to: "docs#pagination", as: :docs_pagination + get "popover", to: "docs#popover", as: :docs_popover + get "progress", to: "docs#progress", as: :docs_progress + get "radio_button", to: "docs#radio_button", as: :docs_radio_button + get "native_select", to: "docs#native_select", as: :docs_native_select + get "select", to: "docs#select", as: :docs_select + get "separator", to: "docs#separator", as: :docs_separator + get "sheet", to: "docs#sheet", as: :docs_sheet + get "shortcut_key", to: "docs#shortcut_key", as: :docs_shortcut_key + get "sidebar", to: "docs#sidebar", as: :docs_sidebar + get "sidebar/example", to: "docs/sidebar#example", as: :docs_sidebar_example + get "sidebar/inset", to: "docs/sidebar#inset_example", as: :docs_sidebar_inset + get "skeleton", to: "docs#skeleton", as: :docs_skeleton + get "switch", to: "docs#switch", as: :docs_switch + get "table", to: "docs#table", as: :docs_table + get "tabs", to: "docs#tabs", as: :docs_tabs + get "textarea", to: "docs#textarea", as: :docs_textarea + get "theme_toggle", to: "docs#theme_toggle", as: :docs_theme_toggle + get "tooltip", to: "docs#tooltip", as: :docs_tooltip + get "typography", to: "docs#typography", as: :docs_typography + end + + match "/404", to: "errors#not_found", via: :all + match "/500", to: "errors#internal_server_error", via: :all + + root "pages#home" +end diff --git a/docs/config/storage.yml b/docs/config/storage.yml new file mode 100644 index 00000000..4942ab66 --- /dev/null +++ b/docs/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/docs/db/migrate/20251120155305_add_service_name_to_active_storage_blobs.active_storage.rb b/docs/db/migrate/20251120155305_add_service_name_to_active_storage_blobs.active_storage.rb new file mode 100644 index 00000000..a15c6ce8 --- /dev/null +++ b/docs/db/migrate/20251120155305_add_service_name_to_active_storage_blobs.active_storage.rb @@ -0,0 +1,22 @@ +# This migration comes from active_storage (originally 20190112182829) +class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] + def up + return unless table_exists?(:active_storage_blobs) + + unless column_exists?(:active_storage_blobs, :service_name) + add_column :active_storage_blobs, :service_name, :string + + if configured_service = ActiveStorage::Blob.service.name + ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) + end + + change_column :active_storage_blobs, :service_name, :string, null: false + end + end + + def down + return unless table_exists?(:active_storage_blobs) + + remove_column :active_storage_blobs, :service_name + end +end diff --git a/docs/db/migrate/20251120155306_create_active_storage_variant_records.active_storage.rb b/docs/db/migrate/20251120155306_create_active_storage_variant_records.active_storage.rb new file mode 100644 index 00000000..94ac83af --- /dev/null +++ b/docs/db/migrate/20251120155306_create_active_storage_variant_records.active_storage.rb @@ -0,0 +1,27 @@ +# This migration comes from active_storage (originally 20191206030411) +class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] + def change + return unless table_exists?(:active_storage_blobs) + + # Use Active Record's configured type for primary key + create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t| + t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type + t.string :variation_digest, null: false + + t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + def primary_key_type + config = Rails.configuration.generators + config.options[config.orm][:primary_key_type] || :primary_key + end + + def blobs_primary_key_type + pkey_name = connection.primary_key(:active_storage_blobs) + pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } + pkey_column.bigint? ? :bigint : pkey_column.type + end +end diff --git a/docs/db/migrate/20251120155307_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb b/docs/db/migrate/20251120155307_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb new file mode 100644 index 00000000..93c8b85a --- /dev/null +++ b/docs/db/migrate/20251120155307_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb @@ -0,0 +1,8 @@ +# This migration comes from active_storage (originally 20211119233751) +class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] + def change + return unless table_exists?(:active_storage_blobs) + + change_column_null(:active_storage_blobs, :checksum, true) + end +end diff --git a/docs/db/schema.rb b/docs/db/schema.rb new file mode 100644 index 00000000..9b04dcfb --- /dev/null +++ b/docs/db/schema.rb @@ -0,0 +1,14 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.1].define(version: 2025_11_20_155307) do +end diff --git a/docs/db/seeds.rb b/docs/db/seeds.rb new file mode 100644 index 00000000..bc25fce3 --- /dev/null +++ b/docs/db/seeds.rb @@ -0,0 +1,7 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Examples: +# +# movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) +# Character.create(name: "Luke", movie: movies.first) diff --git a/docs/fly.toml b/docs/fly.toml new file mode 100644 index 00000000..ce0f4816 --- /dev/null +++ b/docs/fly.toml @@ -0,0 +1,31 @@ +# fly.toml app configuration file generated for ruby-ui on 2024-11-03T18:13:31+01:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'ruby-ui' +primary_region = 'iad' +console_command = '/rails/bin/rails console' + +[build] + +[[mounts]] + source = 'data' + destination = '/data' + +[http_service] + internal_port = 3000 + force_https = true + auto_stop_machines = 'stop' + auto_start_machines = true + min_machines_running = 1 + processes = ['app'] + +[[vm]] + memory = '512mb' + cpu_kind = 'shared' + cpus = 1 + +[[statics]] + guest_path = '/rails/public' + url_prefix = '/' diff --git a/docs/lib/assets/.keep b/docs/lib/assets/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/lib/docs/component_struct.rb b/docs/lib/docs/component_struct.rb new file mode 100644 index 00000000..dd946bf2 --- /dev/null +++ b/docs/lib/docs/component_struct.rb @@ -0,0 +1,12 @@ +# Docs::ComponentStruct = Struct.new(:name, :source, :language, :builder) do +# def initialize(name:, source:, language: :phlex, builder: false) +# super(name, source, language, builder) +# end +# end + +::Docs::ComponentStruct = Struct.new(:name, :source, :builder, :built_using) do + def initialize(name:, source:, builder: false, built_using: :phlex) + source = Views::Base::GITHUB_FILE_URL + source + super(name, source, builder, built_using) + end +end diff --git a/docs/lib/tasks/.keep b/docs/lib/tasks/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/lib/theme/c_s_s.rb b/docs/lib/theme/c_s_s.rb new file mode 100644 index 00000000..50e15487 --- /dev/null +++ b/docs/lib/theme/c_s_s.rb @@ -0,0 +1,528 @@ +module Theme + class CSS + # Ruby UI specific variables that are not part of the standard shadcn theme + RUBY_UI_SPECIFIC_VARS = %w[warning warning-foreground success success-foreground].freeze + + def self.retrieve(theme, with_directive: true, format: :css, exclude_ruby_ui_vars: false) + theme_hash = send(theme) + theme_hash = filter_ruby_ui_vars(theme_hash) if exclude_ruby_ui_vars + + case format + when :css + css = hash_to_css(theme_hash) + with_directive ? wrap_with_directive(css) : css + when :hash + theme_hash + else + raise ArgumentError, "Invalid format: #{format}" + end + end + + def self.filter_ruby_ui_vars(theme_hash) + theme_hash.transform_values do |properties| + properties.reject { |key, _| RUBY_UI_SPECIFIC_VARS.include?(key.to_s) } + end + end + + def self.all_themes + { + default: default, + neutral: neutral, + red: red, + orange: orange, + amber: amber, + yellow: yellow, + lime: lime, + green: green, + emerald: emerald, + teal: teal, + cyan: cyan, + sky: sky, + blue: blue, + indigo: indigo, + violet: violet, + purple: purple, + fuchsia: fuchsia, + pink: pink, + rose: rose + } + end + + def self.default + neutral + end + + def self.neutral + { + root: { + background: "oklch(1 0 0)", + foreground: "oklch(0.145 0 0)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.145 0 0)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.145 0 0)", + primary: "oklch(0.205 0 0)", + "primary-foreground": "oklch(0.985 0 0)", + secondary: "oklch(0.97 0 0)", + "secondary-foreground": "oklch(0.205 0 0)", + muted: "oklch(0.97 0 0)", + "muted-foreground": "oklch(0.556 0 0)", + accent: "oklch(0.97 0 0)", + "accent-foreground": "oklch(0.205 0 0)", + destructive: "oklch(0.577 0.245 27.325)", + "destructive-foreground": "oklch(0.577 0.245 27.325)", + border: "oklch(0.922 0 0)", + input: "oklch(0.922 0 0)", + ring: "oklch(0.708 0 0)", + "chart-1": "oklch(0.646 0.222 41.116)", + "chart-2": "oklch(0.6 0.118 184.704)", + "chart-3": "oklch(0.398 0.07 227.392)", + "chart-4": "oklch(0.828 0.189 84.429)", + "chart-5": "oklch(0.769 0.188 70.08)", + radius: "0.625rem", + sidebar: "oklch(0.985 0 0)", + "sidebar-foreground": "oklch(0.145 0 0)", + "sidebar-primary": "oklch(0.205 0 0)", + "sidebar-primary-foreground": "oklch(0.985 0 0)", + "sidebar-accent": "oklch(0.97 0 0)", + "sidebar-accent-foreground": "oklch(0.205 0 0)", + "sidebar-border": "oklch(0.922 0 0)", + "sidebar-ring": "oklch(0.708 0 0)", + warning: "hsl(38 92% 50%)", + "warning-foreground": "hsl(0 0% 100%)", + success: "hsl(87 100% 37%)", + "success-foreground": "hsl(0 0% 100%)" + }, + dark: { + background: "oklch(0.145 0 0)", + foreground: "oklch(0.985 0 0)", + card: "oklch(0.205 0 0)", + "card-foreground": "oklch(0.985 0 0)", + popover: "oklch(0.205 0 0)", + "popover-foreground": "oklch(0.985 0 0)", + primary: "oklch(0.922 0 0)", + "primary-foreground": "oklch(0.205 0 0)", + secondary: "oklch(0.269 0 0)", + "secondary-foreground": "oklch(0.985 0 0)", + muted: "oklch(0.269 0 0)", + "muted-foreground": "oklch(0.708 0 0)", + accent: "oklch(0.269 0 0)", + "accent-foreground": "oklch(0.985 0 0)", + destructive: "oklch(0.704 0.191 22.216)", + "destructive-foreground": "oklch(0.637 0.237 25.331)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.556 0 0)", + "chart-1": "oklch(0.488 0.243 264.376)", + "chart-2": "oklch(0.696 0.17 162.48)", + "chart-3": "oklch(0.769 0.188 70.08)", + "chart-4": "oklch(0.627 0.265 303.9)", + "chart-5": "oklch(0.645 0.246 16.439)", + sidebar: "oklch(0.205 0 0)", + "sidebar-foreground": "oklch(0.985 0 0)", + "sidebar-primary": "oklch(0.488 0.243 264.376)", + "sidebar-primary-foreground": "oklch(0.985 0 0)", + "sidebar-accent": "oklch(0.269 0 0)", + "sidebar-accent-foreground": "oklch(0.985 0 0)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.556 0 0)", + warning: "hsl(38 92% 50%)", + "warning-foreground": "hsl(0 0% 100%)", + success: "hsl(84 81% 44%)", + "success-foreground": "hsl(0 0% 100%)" + } + } + end + + def self.red + { + root: { + **default_root, + primary: "oklch(0.577 0.245 27.325)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.577 0.245 27.325)" + }, + dark: { + **default_dark, + primary: "oklch(0.396 0.141 25.723)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.396 0.141 25.723)" + } + } + end + + def self.orange + { + root: { + **default_root, + primary: "oklch(0.7048 0.1868 47.6)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.7048 0.1868 47.6)" + }, + dark: { + **default_dark, + primary: "oklch(0.7048 0.1868 47.6)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.7048 0.1868 47.6)" + } + } + end + + def self.amber + { + root: { + **default_root, + primary: "oklch(0.7686 0.1646 70.11)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.7686 0.1646 70.11)" + }, + dark: { + **default_dark, + primary: "oklch(0.7686 0.1646 70.11)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.7686 0.1646 70.11)" + } + } + end + + def self.yellow + { + root: { + **default_root, + primary: "oklch(0.8601 0.173 91.84)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.8601 0.173 91.84)" + }, + dark: { + **default_dark, + primary: "oklch(0.8601 0.173 91.84)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.8601 0.173 91.84)" + } + } + end + + def self.lime + { + root: { + **default_root, + primary: "oklch(0.765 0.2044 131.05)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.765 0.2044 131.05)" + }, + dark: { + **default_dark, + primary: "oklch(0.765 0.2044 131.05)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.765 0.2044 131.05)" + } + } + end + + def self.green + { + root: { + **default_root, + primary: "oklch(0.7205 0.192 149.49)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.7205 0.192 149.49)" + }, + dark: { + **default_dark, + primary: "oklch(0.7205 0.192 149.49)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.7205 0.192 149.49)" + } + } + end + + def self.emerald + { + root: { + **default_root, + primary: "oklch(0.6902 0.1481 162.37)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6902 0.1481 162.37)" + }, + dark: { + **default_dark, + primary: "oklch(0.6902 0.1481 162.37)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6902 0.1481 162.37)" + } + } + end + + def self.teal + { + root: { + **default_root, + primary: "oklch(0.7023 0.1232 181.8)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.7023 0.1232 181.8)" + }, + dark: { + **default_dark, + primary: "oklch(0.7023 0.1232 181.8)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.7023 0.1232 181.8)" + } + } + end + + def self.cyan + { + root: { + **default_root, + primary: "oklch(0.7147 0.126 215.83)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.7147 0.126 215.83)" + }, + dark: { + **default_dark, + primary: "oklch(0.7147 0.126 215.83)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.7147 0.126 215.83)" + } + } + end + + def self.sky + { + root: { + **default_root, + primary: "oklch(0.6847 0.1478 237.27)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6847 0.1478 237.27)" + }, + dark: { + **default_dark, + primary: "oklch(0.6847 0.1478 237.27)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6847 0.1478 237.27)" + } + } + end + + def self.blue + { + root: { + **default_root, + primary: "oklch(0.6232 0.1879 259.8)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6232 0.1879 259.8)" + }, + dark: { + **default_dark, + primary: "oklch(0.6232 0.1879 259.8)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6232 0.1879 259.8)" + } + } + end + + def self.indigo + { + root: { + **default_root, + primary: "oklch(0.5875 0.2039 277.36)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.5875 0.2039 277.36)" + }, + dark: { + **default_dark, + primary: "oklch(0.5875 0.2039 277.36)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.5875 0.2039 277.36)" + } + } + end + + def self.violet + { + root: { + **default_root, + primary: "oklch(0.6016 0.2214 292.23)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6016 0.2214 292.23)" + }, + dark: { + **default_dark, + primary: "oklch(0.6016 0.2214 292.23)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6016 0.2214 292.23)" + } + } + end + + def self.purple + { + root: { + **default_root, + primary: "oklch(0.6268 0.2332 304.11)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6268 0.2332 304.11)" + }, + dark: { + **default_dark, + primary: "oklch(0.6268 0.2332 304.11)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6268 0.2332 304.11)" + } + } + end + + def self.fuchsia + { + root: { + **default_root, + primary: "oklch(0.6683 0.2569 322.02)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6683 0.2569 322.02)" + }, + dark: { + **default_dark, + primary: "oklch(0.6683 0.2569 322.02)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6683 0.2569 322.02)" + } + } + end + + def self.pink + { + root: { + **default_root, + primary: "oklch(0.6538 0.2133 354.06)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6538 0.2133 354.06)" + }, + dark: { + **default_dark, + primary: "oklch(0.6538 0.2133 354.06)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6538 0.2133 354.06)" + } + } + end + + def self.rose + { + root: { + **default_root, + primary: "oklch(0.6437 0.2159 16.81)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6437 0.2159 16.81)" + }, + dark: { + **default_dark, + primary: "oklch(0.6437 0.2159 16.81)", + "primary-foreground": "oklch(0.985 0 0)", + ring: "oklch(0.6437 0.2159 16.81)" + } + } + end + + def self.default_root + { + background: "oklch(1 0 0)", + foreground: "oklch(0.145 0 0)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.145 0 0)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.145 0 0)", + secondary: "oklch(0.97 0 0)", + "secondary-foreground": "oklch(0.205 0 0)", + muted: "oklch(0.97 0 0)", + "muted-foreground": "oklch(0.556 0 0)", + accent: "oklch(0.97 0 0)", + "accent-foreground": "oklch(0.205 0 0)", + destructive: "oklch(0.577 0.245 27.325)", + "destructive-foreground": "oklch(0.577 0.245 27.325)", + border: "oklch(0.922 0 0)", + input: "oklch(0.922 0 0)", + ring: "oklch(0.708 0 0)", + "chart-1": "oklch(0.646 0.222 41.116)", + "chart-2": "oklch(0.6 0.118 184.704)", + "chart-3": "oklch(0.398 0.07 227.392)", + "chart-4": "oklch(0.828 0.189 84.429)", + "chart-5": "oklch(0.769 0.188 70.08)", + radius: "0.625rem", + sidebar: "oklch(0.985 0 0)", + "sidebar-foreground": "oklch(0.145 0 0)", + "sidebar-primary": "oklch(0.205 0 0)", + "sidebar-primary-foreground": "oklch(0.985 0 0)", + "sidebar-accent": "oklch(0.97 0 0)", + "sidebar-accent-foreground": "oklch(0.205 0 0)", + "sidebar-border": "oklch(0.922 0 0)", + "sidebar-ring": "oklch(0.708 0 0)", + warning: "hsl(38 92% 50%)", + "warning-foreground": "hsl(0 0% 100%)", + success: "hsl(87 100% 37%)", + "success-foreground": "hsl(0 0% 100%)" + } + end + + def self.default_dark + { + background: "oklch(0.145 0 0)", + foreground: "oklch(0.985 0 0)", + card: "oklch(0.205 0 0)", + "card-foreground": "oklch(0.985 0 0)", + popover: "oklch(0.205 0 0)", + "popover-foreground": "oklch(0.985 0 0)", + primary: "oklch(0.922 0 0)", + "primary-foreground": "oklch(0.205 0 0)", + secondary: "oklch(0.269 0 0)", + "secondary-foreground": "oklch(0.985 0 0)", + muted: "oklch(0.269 0 0)", + "muted-foreground": "oklch(0.708 0 0)", + accent: "oklch(0.269 0 0)", + "accent-foreground": "oklch(0.985 0 0)", + destructive: "oklch(0.704 0.191 22.216)", + "destructive-foreground": "oklch(0.637 0.237 25.331)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.556 0 0)", + "chart-1": "oklch(0.488 0.243 264.376)", + "chart-2": "oklch(0.696 0.17 162.48)", + "chart-3": "oklch(0.769 0.188 70.08)", + "chart-4": "oklch(0.627 0.265 303.9)", + "chart-5": "oklch(0.645 0.246 16.439)", + sidebar: "oklch(0.205 0 0)", + "sidebar-foreground": "oklch(0.985 0 0)", + "sidebar-primary": "oklch(0.488 0.243 264.376)", + "sidebar-primary-foreground": "oklch(0.985 0 0)", + "sidebar-accent": "oklch(0.269 0 0)", + "sidebar-accent-foreground": "oklch(0.985 0 0)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.556 0 0)", + warning: "hsl(38 92% 50%)", + "warning-foreground": "hsl(0 0% 100%)", + success: "hsl(84 81% 44%)", + "success-foreground": "hsl(0 0% 100%)" + } + end + + def self.hash_to_css(hash) + hash.map do |selector, properties| + "#{format_selector(selector)} {\n" + properties.map { |property, value| " --#{property}: #{value};" }.join("\n") + "\n }" + end.join("\n") + end + + def self.format_selector(selector) + case selector + when :root then ":root" + when :dark then " .dark" # Indentation is important here + else + raise ArgumentError, "Invalid selector: #{selector}" + end + end + + def self.wrap_with_directive(css) + # Tailwind 4: :root and .dark selectors should NOT be wrapped in @layer base + # This ensures CSS variables are properly accessible to @theme inline + css + end + end +end diff --git a/docs/log/.keep b/docs/log/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..5d1c4ed8 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,29 @@ +{ + "name": "app", + "private": true, + "dependencies": { + "@floating-ui/dom": "1.7.6", + "@hotwired/stimulus": "3.2.2", + "@hotwired/turbo-rails": "8.0.23", + "@tailwindcss/forms": "0.5.11", + "@tailwindcss/typography": "0.5.19", + "autoprefixer": "10.5.0", + "chart.js": "4.5.1", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "embla-carousel": "8.6.0", + "esbuild": "0.28.0", + "fuse.js": "7.3.0", + "maska": "3.2.0", + "motion": "12.38.0", + "mustache": "4.2.0", + "tailwindcss": "4.2.2", + "tippy.js": "6.3.7", + "tw-animate-css": "1.4.0" + }, + "scripts": { + "build": "esbuild app/javascript/*.* --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets", + "build:css": "pnpm dlx @tailwindcss/cli -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css --minify" + }, + "packageManager": "pnpm@10.8.0" +} diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 00000000..36a6e1cc --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,660 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@floating-ui/dom': + specifier: 1.7.6 + version: 1.7.6 + '@hotwired/stimulus': + specifier: 3.2.2 + version: 3.2.2 + '@hotwired/turbo-rails': + specifier: 8.0.23 + version: 8.0.23 + '@tailwindcss/forms': + specifier: 0.5.11 + version: 0.5.11(tailwindcss@4.2.2) + '@tailwindcss/typography': + specifier: 0.5.19 + version: 0.5.19(tailwindcss@4.2.2) + autoprefixer: + specifier: 10.5.0 + version: 10.5.0(postcss@8.5.3) + chart.js: + specifier: 4.5.1 + version: 4.5.1 + class-variance-authority: + specifier: 0.7.1 + version: 0.7.1 + clsx: + specifier: 2.1.1 + version: 2.1.1 + embla-carousel: + specifier: 8.6.0 + version: 8.6.0 + esbuild: + specifier: 0.28.0 + version: 0.28.0 + fuse.js: + specifier: 7.3.0 + version: 7.3.0 + maska: + specifier: 3.2.0 + version: 3.2.0 + motion: + specifier: 12.38.0 + version: 12.38.0 + mustache: + specifier: 4.2.0 + version: 4.2.0 + tailwindcss: + specifier: 4.2.2 + version: 4.2.2 + tippy.js: + specifier: 6.3.7 + version: 6.3.7 + tw-animate-css: + specifier: 1.4.0 + version: 1.4.0 + +packages: + + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@hotwired/stimulus@3.2.2': + resolution: {integrity: sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==} + + '@hotwired/turbo-rails@8.0.23': + resolution: {integrity: sha512-iBILwda3qmQC7FYM70+4s6kEQ7Fx9dJ6+yGxjPyrz9a5JDx1+y7OAA5TA7GGVOZJoicMLrKGdFDNorl40X35lw==} + + '@hotwired/turbo@8.0.23': + resolution: {integrity: sha512-GZ7cijxEZ6Ig71u7rD6LHaRv/wcE/hNsc+nEfiWOkLNqUgLOwo5MNGWOy5ZV9ZUDSiQx1no7YxjTNnT4O6//cQ==} + engines: {node: '>= 18'} + + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@rails/actioncable@8.1.200': + resolution: {integrity: sha512-on0DSb7AFUkq1ocxivDNQhhGW/RQpY91zvRVyyaEWP4gOOZWy33P/UyxjQk74IENWNrTqs8+zOGHwTjiiFruRw==} + + '@tailwindcss/forms@0.5.11': + resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' + + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + baseline-browser-mapping@2.10.20: + resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001788: + resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} + + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + electron-to-chromium@1.5.340: + resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==} + + embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + framer-motion@12.38.0: + resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fuse.js@7.3.0: + resolution: {integrity: sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==} + engines: {node: '>=10'} + + maska@3.2.0: + resolution: {integrity: sha512-zSmSgs5/q9vMSmrdZT3rKOv9uLznNWR/niuuAdBZDTvB3SMKOX9vhMtDijFyExz+B4UClu2rvksylUh/ea1bLA==} + + mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + + motion-dom@12.38.0: + resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} + + motion-utils@12.36.0: + resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} + + motion@12.38.0: + resolution: {integrity: sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tailwindcss@4.2.2: + resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} + + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + +snapshots: + + '@esbuild/aix-ppc64@0.28.0': + optional: true + + '@esbuild/android-arm64@0.28.0': + optional: true + + '@esbuild/android-arm@0.28.0': + optional: true + + '@esbuild/android-x64@0.28.0': + optional: true + + '@esbuild/darwin-arm64@0.28.0': + optional: true + + '@esbuild/darwin-x64@0.28.0': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': + optional: true + + '@esbuild/freebsd-x64@0.28.0': + optional: true + + '@esbuild/linux-arm64@0.28.0': + optional: true + + '@esbuild/linux-arm@0.28.0': + optional: true + + '@esbuild/linux-ia32@0.28.0': + optional: true + + '@esbuild/linux-loong64@0.28.0': + optional: true + + '@esbuild/linux-mips64el@0.28.0': + optional: true + + '@esbuild/linux-ppc64@0.28.0': + optional: true + + '@esbuild/linux-riscv64@0.28.0': + optional: true + + '@esbuild/linux-s390x@0.28.0': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.28.0': + optional: true + + '@esbuild/openbsd-arm64@0.28.0': + optional: true + + '@esbuild/openbsd-x64@0.28.0': + optional: true + + '@esbuild/openharmony-arm64@0.28.0': + optional: true + + '@esbuild/sunos-x64@0.28.0': + optional: true + + '@esbuild/win32-arm64@0.28.0': + optional: true + + '@esbuild/win32-ia32@0.28.0': + optional: true + + '@esbuild/win32-x64@0.28.0': + optional: true + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@hotwired/stimulus@3.2.2': {} + + '@hotwired/turbo-rails@8.0.23': + dependencies: + '@hotwired/turbo': 8.0.23 + '@rails/actioncable': 8.1.200 + + '@hotwired/turbo@8.0.23': {} + + '@kurkle/color@0.3.4': {} + + '@popperjs/core@2.11.8': {} + + '@rails/actioncable@8.1.200': {} + + '@tailwindcss/forms@0.5.11(tailwindcss@4.2.2)': + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 4.2.2 + + '@tailwindcss/typography@0.5.19(tailwindcss@4.2.2)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.2.2 + + autoprefixer@10.5.0(postcss@8.5.3): + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001788 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + baseline-browser-mapping@2.10.20: {} + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.20 + caniuse-lite: 1.0.30001788 + electron-to-chromium: 1.5.340 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + caniuse-lite@1.0.30001788: {} + + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clsx@2.1.1: {} + + cssesc@3.0.0: {} + + electron-to-chromium@1.5.340: {} + + embla-carousel@8.6.0: {} + + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + + escalade@3.2.0: {} + + fraction.js@5.3.4: {} + + framer-motion@12.38.0: + dependencies: + motion-dom: 12.38.0 + motion-utils: 12.36.0 + tslib: 2.8.1 + + fuse.js@7.3.0: {} + + maska@3.2.0: {} + + mini-svg-data-uri@1.4.4: {} + + motion-dom@12.38.0: + dependencies: + motion-utils: 12.36.0 + + motion-utils@12.36.0: {} + + motion@12.38.0: + dependencies: + framer-motion: 12.38.0 + tslib: 2.8.1 + + mustache@4.2.0: {} + + nanoid@3.3.11: {} + + node-releases@2.0.37: {} + + picocolors@1.1.1: {} + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.3: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + source-map-js@1.2.1: {} + + tailwindcss@4.2.2: {} + + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + + tslib@2.8.1: {} + + tw-animate-css@1.4.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} diff --git a/docs/public/400.html b/docs/public/400.html new file mode 100644 index 00000000..640de033 --- /dev/null +++ b/docs/public/400.html @@ -0,0 +1,135 @@ + + + + + + + The server cannot process the request due to a client error (400 Bad Request) + + + + + + + + + + + + + +
+
+ +
+
+

The server cannot process the request due to a client error. Please check the request and try again. If you're the application owner check the logs for more information.

+
+
+ + + + diff --git a/docs/public/404.html b/docs/public/404.html new file mode 100644 index 00000000..d7f0f142 --- /dev/null +++ b/docs/public/404.html @@ -0,0 +1,135 @@ + + + + + + + The page you were looking for doesn't exist (404 Not found) + + + + + + + + + + + + + +
+
+ +
+
+

The page you were looking for doesn't exist. You may have mistyped the address or the page may have moved. If you're the application owner check the logs for more information.

+
+
+ + + + diff --git a/docs/public/406-unsupported-browser.html b/docs/public/406-unsupported-browser.html new file mode 100644 index 00000000..43d2811e --- /dev/null +++ b/docs/public/406-unsupported-browser.html @@ -0,0 +1,135 @@ + + + + + + + Your browser is not supported (406 Not Acceptable) + + + + + + + + + + + + + +
+
+ +
+
+

Your browser is not supported.
Please upgrade your browser to continue.

+
+
+ + + + diff --git a/docs/public/422.html b/docs/public/422.html new file mode 100644 index 00000000..f12fb4aa --- /dev/null +++ b/docs/public/422.html @@ -0,0 +1,135 @@ + + + + + + + The change you wanted was rejected (422 Unprocessable Entity) + + + + + + + + + + + + + +
+
+ +
+
+

The change you wanted was rejected. Maybe you tried to change something you didn't have access to. If you're the application owner check the logs for more information.

+
+
+ + + + diff --git a/docs/public/500.html b/docs/public/500.html new file mode 100644 index 00000000..e4eb18a7 --- /dev/null +++ b/docs/public/500.html @@ -0,0 +1,135 @@ + + + + + + + We're sorry, but something went wrong (500 Internal Server Error) + + + + + + + + + + + + + +
+
+ +
+
+

We're sorry, but something went wrong.
If you're the application owner check the logs for more information.

+
+
+ + + + diff --git a/docs/public/android-chrome-192x192.png b/docs/public/android-chrome-192x192.png new file mode 100644 index 00000000..5219e248 Binary files /dev/null and b/docs/public/android-chrome-192x192.png differ diff --git a/docs/public/android-chrome-512x512.png b/docs/public/android-chrome-512x512.png new file mode 100644 index 00000000..01e9b5e2 Binary files /dev/null and b/docs/public/android-chrome-512x512.png differ diff --git a/docs/public/apple-touch-icon.png b/docs/public/apple-touch-icon.png new file mode 100644 index 00000000..8c3c62b0 Binary files /dev/null and b/docs/public/apple-touch-icon.png differ diff --git a/docs/public/favicon-16x16.png b/docs/public/favicon-16x16.png new file mode 100644 index 00000000..ef2407b6 Binary files /dev/null and b/docs/public/favicon-16x16.png differ diff --git a/docs/public/favicon-32x32.png b/docs/public/favicon-32x32.png new file mode 100644 index 00000000..e1689162 Binary files /dev/null and b/docs/public/favicon-32x32.png differ diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico new file mode 100644 index 00000000..9479c777 Binary files /dev/null and b/docs/public/favicon.ico differ diff --git a/docs/public/icon.png b/docs/public/icon.png new file mode 100644 index 00000000..c4c9dbfb Binary files /dev/null and b/docs/public/icon.png differ diff --git a/docs/public/icon.svg b/docs/public/icon.svg new file mode 100644 index 00000000..04b34bf8 --- /dev/null +++ b/docs/public/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/public/robots.txt b/docs/public/robots.txt new file mode 100644 index 00000000..c19f78ab --- /dev/null +++ b/docs/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/docs/public/site.webmanifest b/docs/public/site.webmanifest new file mode 100644 index 00000000..7608d181 --- /dev/null +++ b/docs/public/site.webmanifest @@ -0,0 +1 @@ +{"name":"RubyUI - Component Library","short_name":"RubyUI","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#0a0a0a","display":"standalone"} diff --git a/docs/storage/.keep b/docs/storage/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/test/application_system_test_case.rb b/docs/test/application_system_test_case.rb new file mode 100644 index 00000000..d19212ab --- /dev/null +++ b/docs/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/docs/test/channels/application_cable/connection_test.rb b/docs/test/channels/application_cable/connection_test.rb new file mode 100644 index 00000000..800405f1 --- /dev/null +++ b/docs/test/channels/application_cable/connection_test.rb @@ -0,0 +1,11 @@ +require "test_helper" + +class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase + # test "connects with cookies" do + # cookies.signed[:user_id] = 42 + # + # connect + # + # assert_equal connection.user_id, "42" + # end +end diff --git a/docs/test/controllers/.keep b/docs/test/controllers/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/test/controllers/components_controller_test.rb b/docs/test/controllers/components_controller_test.rb new file mode 100644 index 00000000..148c5811 --- /dev/null +++ b/docs/test/controllers/components_controller_test.rb @@ -0,0 +1,25 @@ +require "test_helper" + +class ComponentsControllerTest < ActionDispatch::IntegrationTest + def self.all_docs_routes + scope_prefix = "/docs" + + Rails.application.routes.routes.select do |route| + route.path.spec.to_s.start_with?(scope_prefix) + end.map do |route| + { + method: route.verb, + path: route.path.spec.to_s.sub(/\(\.:format\)\z/, ""), + controller: route.defaults[:controller], + action: route.defaults[:action] + } + end + end + + all_docs_routes.each do |route| + test "should get #{route[:action]}" do + get route[:path] + assert_response :success + end + end +end diff --git a/docs/test/controllers/docs_controller_test.rb b/docs/test/controllers/docs_controller_test.rb new file mode 100644 index 00000000..da92f149 --- /dev/null +++ b/docs/test/controllers/docs_controller_test.rb @@ -0,0 +1,8 @@ +require "test_helper" + +class DocsControllerTest < ActionDispatch::IntegrationTest + test "should get typography" do + get docs_typography_url + assert_response :success + end +end diff --git a/docs/test/controllers/errors_controller_test.rb b/docs/test/controllers/errors_controller_test.rb new file mode 100644 index 00000000..75fd4dc1 --- /dev/null +++ b/docs/test/controllers/errors_controller_test.rb @@ -0,0 +1,8 @@ +require "test_helper" + +class ErrorsControllerTest < ActionDispatch::IntegrationTest + test "should get not_found" do + get "/invalid_route" + assert_response :not_found + end +end diff --git a/docs/test/controllers/pages_controller_test.rb b/docs/test/controllers/pages_controller_test.rb new file mode 100644 index 00000000..aebf8595 --- /dev/null +++ b/docs/test/controllers/pages_controller_test.rb @@ -0,0 +1,8 @@ +require "test_helper" + +class PagesControllerTest < ActionDispatch::IntegrationTest + test "should get home" do + get root_path + assert_response :success + end +end diff --git a/docs/test/controllers/themes_controller_test.rb b/docs/test/controllers/themes_controller_test.rb new file mode 100644 index 00000000..3f23ed8c --- /dev/null +++ b/docs/test/controllers/themes_controller_test.rb @@ -0,0 +1,8 @@ +require "test_helper" + +class ThemesControllerTest < ActionDispatch::IntegrationTest + test "should get index" do + get theme_path("violet") + assert_response :success + end +end diff --git a/docs/test/controllers/votes_controller_test.rb b/docs/test/controllers/votes_controller_test.rb new file mode 100644 index 00000000..68c091ce --- /dev/null +++ b/docs/test/controllers/votes_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class VotesControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/docs/test/fixtures/files/.keep b/docs/test/fixtures/files/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/test/helpers/.keep b/docs/test/helpers/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/test/integration/.keep b/docs/test/integration/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/test/mailers/.keep b/docs/test/mailers/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/test/models/.keep b/docs/test/models/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/test/system/.keep b/docs/test/system/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/test/test_helper.rb b/docs/test/test_helper.rb new file mode 100644 index 00000000..1f519045 --- /dev/null +++ b/docs/test/test_helper.rb @@ -0,0 +1,19 @@ +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +require "rails/test_help" + +class ActiveSupport::TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... +end + +class ActionDispatch::IntegrationTest + setup do + host! "example.com" + end +end diff --git a/docs/tmp/.keep b/docs/tmp/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/tmp/pids/.keep b/docs/tmp/pids/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/tmp/storage/.keep b/docs/tmp/storage/.keep new file mode 100644 index 00000000..e69de29b diff --git a/docs/vendor/.keep b/docs/vendor/.keep new file mode 100644 index 00000000..e69de29b diff --git a/gem/.devcontainer/Dockerfile b/gem/.devcontainer/Dockerfile new file mode 100644 index 00000000..896970f3 --- /dev/null +++ b/gem/.devcontainer/Dockerfile @@ -0,0 +1,7 @@ +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=3.4.7 +FROM ghcr.io/rails/devcontainer/images/ruby:$RUBY_VERSION + +# Ensure binding is always 0.0.0.0 +# Binds the server to all IP addresses of the container, so it can be accessed from outside the container. +ENV BINDING="0.0.0.0" diff --git a/.devcontainer/compose.yaml b/gem/.devcontainer/compose.yaml similarity index 100% rename from .devcontainer/compose.yaml rename to gem/.devcontainer/compose.yaml diff --git a/.devcontainer/devcontainer.json b/gem/.devcontainer/devcontainer.json similarity index 100% rename from .devcontainer/devcontainer.json rename to gem/.devcontainer/devcontainer.json diff --git a/.gitignore b/gem/.gitignore similarity index 100% rename from .gitignore rename to gem/.gitignore diff --git a/.ruby-version b/gem/.ruby-version similarity index 100% rename from .ruby-version rename to gem/.ruby-version diff --git a/.standard.yml b/gem/.standard.yml similarity index 100% rename from .standard.yml rename to gem/.standard.yml diff --git a/.tool-versions b/gem/.tool-versions similarity index 100% rename from .tool-versions rename to gem/.tool-versions diff --git a/AGENTS.md b/gem/AGENTS.md similarity index 84% rename from AGENTS.md rename to gem/AGENTS.md index cf5c43b1..ea6052d3 100644 --- a/AGENTS.md +++ b/gem/AGENTS.md @@ -1,6 +1,15 @@ # AGENTS.md -This file provides guidance to AI coding agents when working with code in this repository. +This file provides guidance to AI coding agents working in `gem/` (the gem subproject of the RubyUI monorepo). + +## Monorepo context + +This file lives in `gem/`, one of two sibling projects in the `ruby-ui/ruby_ui` monorepo: + +- `gem/` — this directory; the `ruby_ui` gem (Phlex components, generators, tests). +- `docs/` — Rails 8 app that powers https://rubyui.com. Consumes the local gem via `path: "../gem"`. + +When you change a component here (`gem/lib/ruby_ui//`), update the matching documentation view in `docs/app/views/docs/.rb` in the same PR. ## Project Overview diff --git a/CLAUDE.md b/gem/CLAUDE.md similarity index 100% rename from CLAUDE.md rename to gem/CLAUDE.md diff --git a/gem/CONTRIBUTING.md b/gem/CONTRIBUTING.md new file mode 100644 index 00000000..8717420b --- /dev/null +++ b/gem/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# Contributing to RubyUI + +Thank you for your interest in contributing to RubyUI! This document provides guidelines for contributing to the project. + +## Development Setup + +We recommend using the provided devcontainer to set up your development environment. This ensures a consistent environment for all contributors. + +1. Make sure you have Docker +2. Clone the repository +3. Open the project in you editor +4. Select "Reopen in Container" if you are using VSCode or any other method to run the project +5. The devcontainer will set up everything you need to start developing + +## Contribution Process + +1. Fork the repository +2. Create a new branch for your changes +3. Make your changes +4. Run tests to ensure your changes don't break existing functionality: `bundle exec rake test` +5. Run the linter to ensure consistent code formatting: `bundle exec rake standard` +6. Submit a Pull Request to the main repository + +## Focus Areas + +We prioritize: +- Improving existing components rather than adding new ones +- Preserving the shadcn look and feel +- Enhancing documentation +- Fixing bugs + +## Code Standards + +We follow Standard Ruby conventions for code style. The CI pipeline runs `standard` to ensure consistent code formatting. + +## Testing + +While we don't have specific test coverage requirements, all contributions should include tests for new functionality and ensure existing tests pass. + +## Documentation + +If your changes include new components, modify how components should be used, or add new behaviors, please update the corresponding view in `docs/app/views/docs/.rb` in the same PR — the docs Rails app lives in this same monorepo and consumes the local gem via `path: "../gem"`. + +### Installing Documentation Files + +RubyUI includes documentation files for each component that can be installed into your Rails application. These files are located at `lib/ruby_ui/{component}/{component}_docs.rb` and provide usage examples for each component. + +To install the documentation files: + +```bash +bin/rails g ruby_ui:install:docs +``` + +To overwrite existing documentation files: + +```bash +bin/rails g ruby_ui:install:docs --force +``` + +This will copy the documentation files to `app/views/docs/` in your Rails application. + +Thank you for contributing to make RubyUI better! \ No newline at end of file diff --git a/Gemfile b/gem/Gemfile similarity index 100% rename from Gemfile rename to gem/Gemfile diff --git a/Gemfile.lock b/gem/Gemfile.lock similarity index 100% rename from Gemfile.lock rename to gem/Gemfile.lock diff --git a/LICENSE.txt b/gem/LICENSE.txt similarity index 100% rename from LICENSE.txt rename to gem/LICENSE.txt diff --git a/gem/README.md b/gem/README.md new file mode 100644 index 00000000..e3e91119 --- /dev/null +++ b/gem/README.md @@ -0,0 +1,94 @@ +# RubyUI (former PhlexUI) 🚀 + +Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source. + +This is NOT a component library. It's a collection of re-usable components that you can generate or copy and paste into your apps. + +Pick the components you need. Copy and paste the code into your project and customize to your needs. The code is yours. + +Use this as a reference to build your own component libraries. + +### Key Features: + +- **Built for Speed** ⚡: RubyUI leverages Phlex, which is up to 12x faster than traditional Rails ERB templates. +- **Stunning UI** 🎨: Design beautiful, streamlined, and customizable UIs that sell your app effortlessly. +- **Stay Organized** 📁: Keep your UI components well-organized and easy to manage. +- **Customer-Centric UX** 🧑‍💼: Create memorable app experiences for your users. +- **Completely Customizable** 🔧: Full control over the design of all components. +- **Minimal Dependencies** 🍃: Uses custom-built Stimulus.js controllers to keep your app lean. +- **Reuse with Ease** ♻️: Build components once and use them seamlessly across your project. + +### How to Use: + +1. **Find the perfect component** 🔍: Browse live-embedded components on our documentation page. +2. **Copy the snippet** 📋: Easily copy code snippets for quick implementation. +3. **Make it yours** 🎨: Customize components using Tailwind utility classes to fit your specific needs. + +## Installation 🚀 + +> [!NOTE] +> RubyUI 1.0 requires Ruby 3.2 or later + +### 1. Install the gem + +```bash +bundle add ruby_ui --group development --require false +``` + +or add it to your Gemfile: + +```ruby +gem "ruby_ui", group: :development, require: false +``` + +### 2. Run the installer: + +```bash +bin/rails g ruby_ui:install +``` + +### 3. Done! 🎉 + +You can generate your components using `ruby_ui:component` generator. + +```bash +bin/rails g ruby_ui:component Accordion +``` + +You also can generate all components using `ruby_ui:component:all` generator + +## Documentation 📖 + +Visit https://rubyui.com/docs/introduction to view the full documentation, including: + +- Detailed component guides +- Themes +- Lookbook +- Getting started guide + +## Speed Comparison 🏎️ + +RubyUI, powered by Phlex, outperforms alternative methods: + +- Phlex: Baseline 🏁 +- ViewComponent: ~1.5x slower 🚙 +- ERB Templates: ~5x slower 🐢 + +See the original [view layers benchmark](https://github.com/KonnorRogers/view-layer-benchmarks) by @KonnorRogers and its [variations](https://github.com/KonnorRogers/view-layer-benchmarks/forks). + +## Importmap notes: + +If you run into importmap issues this stackoverflow question might help: +https://stackoverflow.com/questions/70548841/how-to-add-custom-js-file-to-new-rails-7-project/72855705 + +## License 📜 + +Licensed under the [MIT license](https://github.com/shadcn/ui/blob/main/LICENSE.md). + +--- + +## Sponsors +[![DigitalOcean Referral Badge](https://web-platforms.sfo2.cdn.digitaloceanspaces.com/WWW/Badge%201.svg)](https://www.digitalocean.com/?refcode=0fdaefc76c39&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge) + + +© 2024 RubyUI. All rights reserved. 🔒 diff --git a/Rakefile b/gem/Rakefile similarity index 100% rename from Rakefile rename to gem/Rakefile diff --git a/bin/console b/gem/bin/console similarity index 100% rename from bin/console rename to gem/bin/console diff --git a/lib/generators/ruby_ui/component/all_generator.rb b/gem/lib/generators/ruby_ui/component/all_generator.rb similarity index 100% rename from lib/generators/ruby_ui/component/all_generator.rb rename to gem/lib/generators/ruby_ui/component/all_generator.rb diff --git a/lib/generators/ruby_ui/component_generator.rb b/gem/lib/generators/ruby_ui/component_generator.rb similarity index 100% rename from lib/generators/ruby_ui/component_generator.rb rename to gem/lib/generators/ruby_ui/component_generator.rb diff --git a/lib/generators/ruby_ui/dependencies.yml b/gem/lib/generators/ruby_ui/dependencies.yml similarity index 100% rename from lib/generators/ruby_ui/dependencies.yml rename to gem/lib/generators/ruby_ui/dependencies.yml diff --git a/lib/generators/ruby_ui/install/docs_generator.rb b/gem/lib/generators/ruby_ui/install/docs_generator.rb similarity index 100% rename from lib/generators/ruby_ui/install/docs_generator.rb rename to gem/lib/generators/ruby_ui/install/docs_generator.rb diff --git a/lib/generators/ruby_ui/install/install_generator.rb b/gem/lib/generators/ruby_ui/install/install_generator.rb similarity index 100% rename from lib/generators/ruby_ui/install/install_generator.rb rename to gem/lib/generators/ruby_ui/install/install_generator.rb diff --git a/lib/generators/ruby_ui/install/templates/ruby_ui.rb.erb b/gem/lib/generators/ruby_ui/install/templates/ruby_ui.rb.erb similarity index 100% rename from lib/generators/ruby_ui/install/templates/ruby_ui.rb.erb rename to gem/lib/generators/ruby_ui/install/templates/ruby_ui.rb.erb diff --git a/lib/generators/ruby_ui/install/templates/tailwind.css.erb b/gem/lib/generators/ruby_ui/install/templates/tailwind.css.erb similarity index 100% rename from lib/generators/ruby_ui/install/templates/tailwind.css.erb rename to gem/lib/generators/ruby_ui/install/templates/tailwind.css.erb diff --git a/lib/generators/ruby_ui/javascript_utils.rb b/gem/lib/generators/ruby_ui/javascript_utils.rb similarity index 100% rename from lib/generators/ruby_ui/javascript_utils.rb rename to gem/lib/generators/ruby_ui/javascript_utils.rb diff --git a/lib/ruby_ui.rb b/gem/lib/ruby_ui.rb similarity index 100% rename from lib/ruby_ui.rb rename to gem/lib/ruby_ui.rb diff --git a/lib/ruby_ui/accordion/accordion.rb b/gem/lib/ruby_ui/accordion/accordion.rb similarity index 100% rename from lib/ruby_ui/accordion/accordion.rb rename to gem/lib/ruby_ui/accordion/accordion.rb diff --git a/lib/ruby_ui/accordion/accordion_content.rb b/gem/lib/ruby_ui/accordion/accordion_content.rb similarity index 100% rename from lib/ruby_ui/accordion/accordion_content.rb rename to gem/lib/ruby_ui/accordion/accordion_content.rb diff --git a/gem/lib/ruby_ui/accordion/accordion_controller.js b/gem/lib/ruby_ui/accordion/accordion_controller.js new file mode 100644 index 00000000..2408ce7f --- /dev/null +++ b/gem/lib/ruby_ui/accordion/accordion_controller.js @@ -0,0 +1,97 @@ +import { Controller } from "@hotwired/stimulus"; +import { animate } from "motion"; + +// Connects to data-controller="ruby-ui--accordion" +export default class extends Controller { + static targets = ["icon", "content"]; + static values = { + open: { + type: Boolean, + default: false, + }, + animationDuration: { + type: Number, + default: 0.15, // Default animation duration (in seconds) + }, + animationEasing: { + type: String, + default: "ease-in-out", // Default animation easing + }, + rotateIcon: { + type: Number, + default: 180, // Default icon rotation (in degrees) + }, + }; + + connect() { + // Set the initial state of the accordion + let originalAnimationDuration = this.animationDurationValue; + this.animationDurationValue = 0; + this.openValue ? this.open() : this.close(); + this.animationDurationValue = originalAnimationDuration; + } + + // Toggle the 'open' value + toggle() { + this.openValue = !this.openValue; + } + + // Handle changes in the 'open' value + openValueChanged(isOpen, wasOpen) { + if (isOpen) { + this.open(); + } else { + this.close(); + } + } + + // Open the accordion content + open() { + if (this.hasContentTarget) { + this.revealContent(); + this.hasIconTarget && this.rotateIcon(); + this.openValue = true; + } + } + + // Close the accordion content + close() { + if (this.hasContentTarget) { + this.hideContent(); + this.hasIconTarget && this.rotateIcon(); + this.openValue = false; + } + } + + // Reveal the accordion content with animation + revealContent() { + const contentHeight = this.contentTarget.scrollHeight; + animate( + this.contentTarget, + { height: `${contentHeight}px` }, + { + duration: this.animationDurationValue, + easing: this.animationEasingValue, + }, + ); + } + + // Hide the accordion content with animation + hideContent() { + animate( + this.contentTarget, + { height: 0 }, + { + duration: this.animationDurationValue, + easing: this.animationEasingValue, + }, + ); + } + + // Rotate the accordion icon 180deg using animate function + rotateIcon() { + animate(this.iconTarget, { + rotate: `${this.openValue ? this.rotateIconValue : 0}deg`, + }); + } +} diff --git a/lib/ruby_ui/accordion/accordion_default_content.rb b/gem/lib/ruby_ui/accordion/accordion_default_content.rb similarity index 100% rename from lib/ruby_ui/accordion/accordion_default_content.rb rename to gem/lib/ruby_ui/accordion/accordion_default_content.rb diff --git a/lib/ruby_ui/accordion/accordion_default_trigger.rb b/gem/lib/ruby_ui/accordion/accordion_default_trigger.rb similarity index 100% rename from lib/ruby_ui/accordion/accordion_default_trigger.rb rename to gem/lib/ruby_ui/accordion/accordion_default_trigger.rb diff --git a/lib/ruby_ui/accordion/accordion_docs.rb b/gem/lib/ruby_ui/accordion/accordion_docs.rb similarity index 100% rename from lib/ruby_ui/accordion/accordion_docs.rb rename to gem/lib/ruby_ui/accordion/accordion_docs.rb diff --git a/lib/ruby_ui/accordion/accordion_icon.rb b/gem/lib/ruby_ui/accordion/accordion_icon.rb similarity index 100% rename from lib/ruby_ui/accordion/accordion_icon.rb rename to gem/lib/ruby_ui/accordion/accordion_icon.rb diff --git a/lib/ruby_ui/accordion/accordion_item.rb b/gem/lib/ruby_ui/accordion/accordion_item.rb similarity index 100% rename from lib/ruby_ui/accordion/accordion_item.rb rename to gem/lib/ruby_ui/accordion/accordion_item.rb diff --git a/lib/ruby_ui/accordion/accordion_trigger.rb b/gem/lib/ruby_ui/accordion/accordion_trigger.rb similarity index 100% rename from lib/ruby_ui/accordion/accordion_trigger.rb rename to gem/lib/ruby_ui/accordion/accordion_trigger.rb diff --git a/lib/ruby_ui/alert/alert.rb b/gem/lib/ruby_ui/alert/alert.rb similarity index 100% rename from lib/ruby_ui/alert/alert.rb rename to gem/lib/ruby_ui/alert/alert.rb diff --git a/lib/ruby_ui/alert/alert_description.rb b/gem/lib/ruby_ui/alert/alert_description.rb similarity index 100% rename from lib/ruby_ui/alert/alert_description.rb rename to gem/lib/ruby_ui/alert/alert_description.rb diff --git a/gem/lib/ruby_ui/alert/alert_docs.rb b/gem/lib/ruby_ui/alert/alert_docs.rb new file mode 100644 index 00000000..5211074c --- /dev/null +++ b/gem/lib/ruby_ui/alert/alert_docs.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +class Views::Docs::Alert < Views::Base + def view_template + component = "Alert" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Alert", description: "Displays a callout for user attention.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + Alert do + rocket_icon + AlertTitle { "Pro tip" } + AlertDescription { "With RubyUI you'll ship faster." } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Without Icon", context: self) do + <<~RUBY + Alert do + AlertTitle { "Pro tip" } + AlertDescription { "Simply, don't include an icon and your alert will look like this." } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Warning", context: self) do + <<~RUBY + Alert(variant: :warning) do + info_icon + AlertTitle { "Ship often" } + AlertDescription { "Shipping is good, your users will thank you for it." } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Destructive", context: self) do + <<~RUBY + Alert(variant: :destructive) do + alert_icon + AlertTitle { "Oopsie daisy!" } + AlertDescription { "Your design system is non-existent." } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Success", context: self) do + <<~RUBY + Alert(variant: :success) do + check_icon + AlertTitle { "Installation successful" } + AlertDescription { "You're all set to start using RubyUI in your application." } + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end + + private + + def rocket_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M9.315 7.584C12.195 3.883 16.695 1.5 21.75 1.5a.75.75 0 01.75.75c0 5.056-2.383 9.555-6.084 12.436A6.75 6.75 0 019.75 22.5a.75.75 0 01-.75-.75v-4.131A15.838 15.838 0 016.382 15H2.25a.75.75 0 01-.75-.75 6.75 6.75 0 017.815-6.666zM15 6.75a2.25 2.25 0 100 4.5 2.25 2.25 0 000-4.5z", + clip_rule: "evenodd" + ) + s.path( + d: + "M5.26 17.242a.75.75 0 10-.897-1.203 5.243 5.243 0 00-2.05 5.022.75.75 0 00.625.627 5.243 5.243 0 005.022-2.051.75.75 0 10-1.202-.897 3.744 3.744 0 01-3.008 1.51c0-1.23.592-2.323 1.51-3.008z" + ) + end + end + + def alert_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z", + clip_rule: "evenodd" + ) + end + end + + def info_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 01.67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 11-.671-1.34l.041-.022zM12 9a.75.75 0 100-1.5.75.75 0 000 1.5z", + clip_rule: "evenodd" + ) + end + end + + def check_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z", + clip_rule: "evenodd" + ) + end + end +end diff --git a/lib/ruby_ui/alert/alert_title.rb b/gem/lib/ruby_ui/alert/alert_title.rb similarity index 100% rename from lib/ruby_ui/alert/alert_title.rb rename to gem/lib/ruby_ui/alert/alert_title.rb diff --git a/lib/ruby_ui/alert_dialog/alert_dialog.rb b/gem/lib/ruby_ui/alert_dialog/alert_dialog.rb similarity index 100% rename from lib/ruby_ui/alert_dialog/alert_dialog.rb rename to gem/lib/ruby_ui/alert_dialog/alert_dialog.rb diff --git a/lib/ruby_ui/alert_dialog/alert_dialog_action.rb b/gem/lib/ruby_ui/alert_dialog/alert_dialog_action.rb similarity index 100% rename from lib/ruby_ui/alert_dialog/alert_dialog_action.rb rename to gem/lib/ruby_ui/alert_dialog/alert_dialog_action.rb diff --git a/lib/ruby_ui/alert_dialog/alert_dialog_cancel.rb b/gem/lib/ruby_ui/alert_dialog/alert_dialog_cancel.rb similarity index 100% rename from lib/ruby_ui/alert_dialog/alert_dialog_cancel.rb rename to gem/lib/ruby_ui/alert_dialog/alert_dialog_cancel.rb diff --git a/lib/ruby_ui/alert_dialog/alert_dialog_content.rb b/gem/lib/ruby_ui/alert_dialog/alert_dialog_content.rb similarity index 100% rename from lib/ruby_ui/alert_dialog/alert_dialog_content.rb rename to gem/lib/ruby_ui/alert_dialog/alert_dialog_content.rb diff --git a/gem/lib/ruby_ui/alert_dialog/alert_dialog_controller.js b/gem/lib/ruby_ui/alert_dialog/alert_dialog_controller.js new file mode 100644 index 00000000..98952e95 --- /dev/null +++ b/gem/lib/ruby_ui/alert_dialog/alert_dialog_controller.js @@ -0,0 +1,31 @@ +import { Controller } from "@hotwired/stimulus"; + +// Connects to data-controller="ruby-ui--alert-dialog" +export default class extends Controller { + static targets = ["content"]; + static values = { + open: { + type: Boolean, + default: false, + }, + }; + + connect() { + if (this.openValue) { + this.open(); + } + } + + open() { + document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML); + // prevent scroll on body + document.body.classList.add("overflow-hidden"); + } + + dismiss(e) { + // allow scroll on body + document.body.classList.remove("overflow-hidden"); + // remove the element + this.element.remove(); + } +} diff --git a/lib/ruby_ui/alert_dialog/alert_dialog_description.rb b/gem/lib/ruby_ui/alert_dialog/alert_dialog_description.rb similarity index 100% rename from lib/ruby_ui/alert_dialog/alert_dialog_description.rb rename to gem/lib/ruby_ui/alert_dialog/alert_dialog_description.rb diff --git a/gem/lib/ruby_ui/alert_dialog/alert_dialog_docs.rb b/gem/lib/ruby_ui/alert_dialog/alert_dialog_docs.rb new file mode 100644 index 00000000..43f17654 --- /dev/null +++ b/gem/lib/ruby_ui/alert_dialog/alert_dialog_docs.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class Views::Docs::AlertDialog < Views::Base + def view_template + component = "AlertDialog" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Alert Dialog", description: "A modal dialog that interrupts the user with important content and expects a response.") + + Heading(level: 2) { "Usage" } + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + AlertDialog do + AlertDialogTrigger do + Button { "Show dialog" } + end + AlertDialogContent do + AlertDialogHeader do + AlertDialogTitle { "Are you absolutely sure?" } + AlertDialogDescription { "This action cannot be undone. This will permanently delete your account and remove your data from our servers." } + end + AlertDialogFooter do + AlertDialogCancel { "Cancel" } + AlertDialogAction { "Continue" } # Will probably be a link to a controller action (e.g. delete account) + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/alert_dialog/alert_dialog_footer.rb b/gem/lib/ruby_ui/alert_dialog/alert_dialog_footer.rb similarity index 100% rename from lib/ruby_ui/alert_dialog/alert_dialog_footer.rb rename to gem/lib/ruby_ui/alert_dialog/alert_dialog_footer.rb diff --git a/lib/ruby_ui/alert_dialog/alert_dialog_header.rb b/gem/lib/ruby_ui/alert_dialog/alert_dialog_header.rb similarity index 100% rename from lib/ruby_ui/alert_dialog/alert_dialog_header.rb rename to gem/lib/ruby_ui/alert_dialog/alert_dialog_header.rb diff --git a/lib/ruby_ui/alert_dialog/alert_dialog_title.rb b/gem/lib/ruby_ui/alert_dialog/alert_dialog_title.rb similarity index 100% rename from lib/ruby_ui/alert_dialog/alert_dialog_title.rb rename to gem/lib/ruby_ui/alert_dialog/alert_dialog_title.rb diff --git a/lib/ruby_ui/alert_dialog/alert_dialog_trigger.rb b/gem/lib/ruby_ui/alert_dialog/alert_dialog_trigger.rb similarity index 100% rename from lib/ruby_ui/alert_dialog/alert_dialog_trigger.rb rename to gem/lib/ruby_ui/alert_dialog/alert_dialog_trigger.rb diff --git a/lib/ruby_ui/aspect_ratio/aspect_ratio.rb b/gem/lib/ruby_ui/aspect_ratio/aspect_ratio.rb similarity index 100% rename from lib/ruby_ui/aspect_ratio/aspect_ratio.rb rename to gem/lib/ruby_ui/aspect_ratio/aspect_ratio.rb diff --git a/gem/lib/ruby_ui/aspect_ratio/aspect_ratio_docs.rb b/gem/lib/ruby_ui/aspect_ratio/aspect_ratio_docs.rb new file mode 100644 index 00000000..760a713f --- /dev/null +++ b/gem/lib/ruby_ui/aspect_ratio/aspect_ratio_docs.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +class Views::Docs::AspectRatio < Views::Base + def view_template + component = "AspectRatio" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Aspect Ratio", description: "Displays content within a desired ratio.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "16/9", context: self) do + <<~RUBY + AspectRatio(aspect_ratio: "16/9", class: "rounded-md overflow-hidden border shadow-sm") do + img( + alt: "Placeholder", + loading: "lazy", + src: image_path('pattern.jpg') + ) + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "4/3", context: self) do + <<~RUBY + AspectRatio(aspect_ratio: "4/3", class: "rounded-md overflow-hidden border shadow-sm") do + img( + alt: "Placeholder", + loading: "lazy", + src: image_path('pattern.jpg') + ) + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "1/1", context: self) do + <<~RUBY + AspectRatio(aspect_ratio: "1/1", class: "rounded-md overflow-hidden border shadow-sm") do + img( + alt: "Placeholder", + loading: "lazy", + src: image_path('pattern.jpg') + ) + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "21/9", context: self) do + <<~RUBY + AspectRatio(aspect_ratio: "21/9", class: "rounded-md overflow-hidden border shadow-sm") do + img( + alt: "Placeholder", + loading: "lazy", + src: image_path('pattern.jpg') + ) + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/avatar/avatar.rb b/gem/lib/ruby_ui/avatar/avatar.rb similarity index 100% rename from lib/ruby_ui/avatar/avatar.rb rename to gem/lib/ruby_ui/avatar/avatar.rb diff --git a/gem/lib/ruby_ui/avatar/avatar_docs.rb b/gem/lib/ruby_ui/avatar/avatar_docs.rb new file mode 100644 index 00000000..4f5e4c35 --- /dev/null +++ b/gem/lib/ruby_ui/avatar/avatar_docs.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +class Views::Docs::Avatar < Views::Base + def view_template + component = "Avatar" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Avatar", description: "An image element with a fallback for representing the user.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Image & fallback", context: self) do + <<~RUBY + Avatar do + AvatarImage(src: "https://avatars.githubusercontent.com/u/246692?v=4", alt: "joeldrapper") + AvatarFallback { "JD" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Only fallback", context: self) do + <<~RUBY + Avatar do + AvatarFallback { "JD" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Sizes", context: self) do + <<~RUBY + div(class: 'flex items-center space-x-2') do + # size: :xs + Avatar(size: :xs) do + AvatarImage(src: "https://avatars.githubusercontent.com/u/246692?v=4", alt: "joeldrapper") + AvatarFallback { "JD" } + end + # size: :sm + Avatar(size: :sm) do + AvatarImage(src: "https://avatars.githubusercontent.com/u/246692?v=4", alt: "joeldrapper") + AvatarFallback { "JD" } + end + # size: :md + Avatar(size: :md) do + AvatarImage(src: "https://avatars.githubusercontent.com/u/246692?v=4", alt: "joeldrapper") + AvatarFallback { "JD" } + end + # size: :lg + Avatar(size: :lg) do + AvatarImage(src: "https://avatars.githubusercontent.com/u/246692?v=4", alt: "joeldrapper") + AvatarFallback { "JD" } + end + # size: :xl + Avatar(size: :xl) do + AvatarImage(src: "https://avatars.githubusercontent.com/u/246692?v=4", alt: "joeldrapper") + AvatarFallback { "JD" } + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Sizes (only fallback)", context: self) do + @@code = <<~RUBY + div(class: 'flex items-center space-x-2') do + # size: :xs + Avatar(size: :xs) do + AvatarFallback { "JD" } + end + # size: :sm + Avatar(size: :sm) do + AvatarFallback { "JD" } + end + # size: :md + Avatar(size: :md) do + AvatarFallback { "JD" } + end + # size: :lg + Avatar(size: :lg) do + AvatarFallback { "JD" } + end + # size: :xl + Avatar(size: :xl) do + AvatarFallback { "JD" } + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/avatar/avatar_fallback.rb b/gem/lib/ruby_ui/avatar/avatar_fallback.rb similarity index 100% rename from lib/ruby_ui/avatar/avatar_fallback.rb rename to gem/lib/ruby_ui/avatar/avatar_fallback.rb diff --git a/lib/ruby_ui/avatar/avatar_image.rb b/gem/lib/ruby_ui/avatar/avatar_image.rb similarity index 100% rename from lib/ruby_ui/avatar/avatar_image.rb rename to gem/lib/ruby_ui/avatar/avatar_image.rb diff --git a/lib/ruby_ui/badge/badge.rb b/gem/lib/ruby_ui/badge/badge.rb similarity index 100% rename from lib/ruby_ui/badge/badge.rb rename to gem/lib/ruby_ui/badge/badge.rb diff --git a/gem/lib/ruby_ui/badge/badge_docs.rb b/gem/lib/ruby_ui/badge/badge_docs.rb new file mode 100644 index 00000000..9687663a --- /dev/null +++ b/gem/lib/ruby_ui/badge/badge_docs.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +class Views::Docs::Badge < Views::Base + def view_template + component = "Badge" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Badge", description: "Displays a badge or a component that looks like a badge.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Default", context: self) do + <<~RUBY + Badge { "Badge" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Primary", context: self) do + <<~RUBY + Badge(variant: :primary) { 'Primary' } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Outline", context: self) do + <<~RUBY + Badge(variant: :outline) { 'Outline' } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Variants", context: self) do + <<~RUBY + div(class: 'flex flex-wrap gap-2 justify-center') do + Badge(variant: :destructive) { 'Destructive' } + Badge(variant: :warning) { 'Warning' } + Badge(variant: :success) { 'Success' } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Other Colors", context: self) do + <<~RUBY + div(class: 'flex flex-wrap gap-2 justify-center') do + Badge(variant: :red) { 'Red' } + Badge(variant: :orange) { 'Orange' } + Badge(variant: :amber) { 'Amber' } + Badge(variant: :yellow) { 'Yellow' } + Badge(variant: :lime) { 'Lime' } + Badge(variant: :green) { 'Green' } + Badge(variant: :emerald) { 'Emerald' } + Badge(variant: :teal) { 'Teal' } + Badge(variant: :cyan) { 'Cyan' } + Badge(variant: :sky) { 'Sky' } + Badge(variant: :blue) { 'Blue' } + Badge(variant: :indigo) { 'Indigo' } + Badge(variant: :violet) { 'Violet' } + Badge(variant: :purple) { 'Purple' } + Badge(variant: :fuchsia) { 'Fuchsia' } + Badge(variant: :pink) { 'Pink' } + Badge(variant: :rose) { 'Rose' } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Sizes", context: self) do + <<~RUBY + div(class: 'flex flex-wrap gap-2 justify-center items-center') do + Badge(size: :sm) { "Small" } + Badge(size: :md) { "Medium" } + Badge(size: :lg) { "Large" } + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + # components + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/base.rb b/gem/lib/ruby_ui/base.rb similarity index 100% rename from lib/ruby_ui/base.rb rename to gem/lib/ruby_ui/base.rb diff --git a/lib/ruby_ui/breadcrumb/breadcrumb.rb b/gem/lib/ruby_ui/breadcrumb/breadcrumb.rb similarity index 100% rename from lib/ruby_ui/breadcrumb/breadcrumb.rb rename to gem/lib/ruby_ui/breadcrumb/breadcrumb.rb diff --git a/gem/lib/ruby_ui/breadcrumb/breadcrumb_docs.rb b/gem/lib/ruby_ui/breadcrumb/breadcrumb_docs.rb new file mode 100644 index 00000000..127014d2 --- /dev/null +++ b/gem/lib/ruby_ui/breadcrumb/breadcrumb_docs.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +class Views::Docs::Breadcrumb < Views::Base + def view_template + component = "Breadcrumb" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Breadcrumb", description: "Indicates the user's current location within a navigational hierarchy.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + Breadcrumb do + BreadcrumbList do + BreadcrumbItem do + BreadcrumbLink(href: "/") { "Home" } + end + BreadcrumbSeparator() + BreadcrumbItem do + BreadcrumbLink(href: "/docs/accordion") { "Components" } + end + BreadcrumbSeparator() + BreadcrumbItem do + BreadcrumbPage { "Breadcrumb" } + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "With custom separator", context: self) do + <<~RUBY + Breadcrumb do + BreadcrumbList do + BreadcrumbItem do + BreadcrumbLink(href: "/") { "Home" } + end + BreadcrumbSeparator { slash_icon } + BreadcrumbItem do + BreadcrumbLink(href: "/docs/accordion") { "Components" } + end + BreadcrumbSeparator { slash_icon } + BreadcrumbItem do + BreadcrumbPage { "Breadcrumb" } + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Collapsed", context: self) do + <<~RUBY + Breadcrumb do + BreadcrumbList do + BreadcrumbItem do + BreadcrumbLink(href: "/") { "Home" } + end + BreadcrumbSeparator() + BreadcrumbItem do + BreadcrumbEllipsis() + end + BreadcrumbSeparator() + BreadcrumbItem do + BreadcrumbLink(href: "/docs/accordion") { "Components" } + end + BreadcrumbSeparator() + BreadcrumbItem do + BreadcrumbPage { "Breadcrumb" } + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "With Link component", context: self) do + <<~RUBY + Breadcrumb do + BreadcrumbList do + BreadcrumbItem do + BreadcrumbLink(href: "/") { "Home" } + end + BreadcrumbSeparator() + BreadcrumbItem do + Link(href: "/docs/accordion", class: "px-0") { "Components" } + end + BreadcrumbSeparator() + BreadcrumbItem do + BreadcrumbPage { "Breadcrumb" } + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end + + private + + def slash_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + class: "w-4 h-4", + viewbox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round" + ) { |s| s.path(d: "M22 2 2 22") } + end +end diff --git a/lib/ruby_ui/breadcrumb/breadcrumb_ellipsis.rb b/gem/lib/ruby_ui/breadcrumb/breadcrumb_ellipsis.rb similarity index 100% rename from lib/ruby_ui/breadcrumb/breadcrumb_ellipsis.rb rename to gem/lib/ruby_ui/breadcrumb/breadcrumb_ellipsis.rb diff --git a/lib/ruby_ui/breadcrumb/breadcrumb_item.rb b/gem/lib/ruby_ui/breadcrumb/breadcrumb_item.rb similarity index 100% rename from lib/ruby_ui/breadcrumb/breadcrumb_item.rb rename to gem/lib/ruby_ui/breadcrumb/breadcrumb_item.rb diff --git a/lib/ruby_ui/breadcrumb/breadcrumb_link.rb b/gem/lib/ruby_ui/breadcrumb/breadcrumb_link.rb similarity index 100% rename from lib/ruby_ui/breadcrumb/breadcrumb_link.rb rename to gem/lib/ruby_ui/breadcrumb/breadcrumb_link.rb diff --git a/lib/ruby_ui/breadcrumb/breadcrumb_list.rb b/gem/lib/ruby_ui/breadcrumb/breadcrumb_list.rb similarity index 100% rename from lib/ruby_ui/breadcrumb/breadcrumb_list.rb rename to gem/lib/ruby_ui/breadcrumb/breadcrumb_list.rb diff --git a/lib/ruby_ui/breadcrumb/breadcrumb_page.rb b/gem/lib/ruby_ui/breadcrumb/breadcrumb_page.rb similarity index 100% rename from lib/ruby_ui/breadcrumb/breadcrumb_page.rb rename to gem/lib/ruby_ui/breadcrumb/breadcrumb_page.rb diff --git a/lib/ruby_ui/breadcrumb/breadcrumb_separator.rb b/gem/lib/ruby_ui/breadcrumb/breadcrumb_separator.rb similarity index 100% rename from lib/ruby_ui/breadcrumb/breadcrumb_separator.rb rename to gem/lib/ruby_ui/breadcrumb/breadcrumb_separator.rb diff --git a/lib/ruby_ui/button/button.rb b/gem/lib/ruby_ui/button/button.rb similarity index 100% rename from lib/ruby_ui/button/button.rb rename to gem/lib/ruby_ui/button/button.rb diff --git a/gem/lib/ruby_ui/button/button_docs.rb b/gem/lib/ruby_ui/button/button_docs.rb new file mode 100644 index 00000000..ee943b12 --- /dev/null +++ b/gem/lib/ruby_ui/button/button_docs.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +class Views::Docs::Button < Views::Base + def view_template + component = "Button" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Button", description: "Displays a button or a component that looks like a button.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + Button { "Button" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Primary", context: self) do + <<~RUBY + Button(variant: :primary) { "Primary" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Secondary", context: self) do + <<~RUBY + Button(variant: :secondary) { "Secondary" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Destructive", context: self) do + <<~RUBY + Button(variant: :destructive) { "Destructive" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Outline", context: self) do + <<~RUBY + Button(variant: :outline) { "Outline" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Ghost", context: self) do + <<~RUBY + Button(variant: :ghost) { "Ghost" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Link", context: self) do + <<~RUBY + Button(variant: :link) { "Link" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Disabled", context: self) do + <<~RUBY + Button(disabled: true) { "Disabled" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Aria Disabled", context: self) do + <<~RUBY + Button(aria: {disabled: "true"}) { "Aria Disabled" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Icon", context: self) do + <<~RUBY + Button(variant: :outline, icon: true) do + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 20 20", + fill: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z", + clip_rule: "evenodd" + ) + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "With Icon", context: self) do + <<~RUBY + Button(variant: :primary) do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" + ) + end + span { "Login with Email" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "With Icon", context: self) do + <<~RUBY + Button(variant: :primary, disabled: true) do + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 20 20", + fill: "currentColor", + class: "w-4 h-4 mr-2 animate-spin" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z", + clip_rule: "evenodd" + ) + end + span { "Please wait" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Submit", context: self) do + <<~RUBY + Button(variant: :primary, type: :submit) do + span { "Submit application" } + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/calendar/calendar.rb b/gem/lib/ruby_ui/calendar/calendar.rb similarity index 100% rename from lib/ruby_ui/calendar/calendar.rb rename to gem/lib/ruby_ui/calendar/calendar.rb diff --git a/lib/ruby_ui/calendar/calendar_body.rb b/gem/lib/ruby_ui/calendar/calendar_body.rb similarity index 100% rename from lib/ruby_ui/calendar/calendar_body.rb rename to gem/lib/ruby_ui/calendar/calendar_body.rb diff --git a/gem/lib/ruby_ui/calendar/calendar_controller.js b/gem/lib/ruby_ui/calendar/calendar_controller.js new file mode 100644 index 00000000..e1b47c04 --- /dev/null +++ b/gem/lib/ruby_ui/calendar/calendar_controller.js @@ -0,0 +1,249 @@ +import { Controller } from "@hotwired/stimulus"; +import Mustache from "mustache"; + +export default class extends Controller { + static targets = [ + "calendar", + "title", + "weekdaysTemplate", + "selectedDateTemplate", + "todayDateTemplate", + "currentMonthDateTemplate", + "otherMonthDateTemplate", + ]; + static values = { + selectedDate: { + type: String, + default: null, + }, + viewDate: { + type: String, + default: new Date().toISOString().slice(0, 10), + }, + format: { + type: String, + default: "yyyy-MM-dd", // Default format + }, + }; + static outlets = ["ruby-ui--calendar-input"]; + + initialize() { + this.updateCalendar(); // Initial calendar render + } + + nextMonth(e) { + e.preventDefault(); + this.viewDateValue = this.adjustMonth(1); + } + + prevMonth(e) { + e.preventDefault(); + this.viewDateValue = this.adjustMonth(-1); + } + + selectDay(e) { + e.preventDefault(); + // Set the selected date value + this.selectedDateValue = e.currentTarget.dataset.day; + } + + selectedDateValueChanged(value, prevValue) { + // update the viewDateValue to the first day of month of the selected date (This will trigger updateCalendar() function) + const newViewDate = new Date(this.selectedDateValue); + newViewDate.setDate(2); // set the day to the 2nd (to avoid issues with months with different number of days and timezones) + this.viewDateValue = newViewDate.toISOString().slice(0, 10); + + // Re-render the calendar + this.updateCalendar(); + + // update the input value + this.rubyUiCalendarInputOutlets.forEach((outlet) => { + const formattedDate = this.formatDate(this.selectedDate()); + outlet.setValue(formattedDate); + }); + } + + viewDateValueChanged(value, prevValue) { + this.updateCalendar(); + } + + adjustMonth(adjustment) { + const date = this.viewDate(); + date.setDate(2); // set the day to the 2nd (to avoid issues with months with different number of days and timezones) + date.setMonth(date.getMonth() + adjustment); + return date.toISOString().slice(0, 10); + } + + updateCalendar() { + // Update the title with month and year + this.titleTarget.textContent = this.monthAndYear(); + this.calendarTarget.innerHTML = this.calendarHTML(); + } + + calendarHTML() { + return this.weekdaysTemplateTarget.innerHTML + this.calendarDays(); + } + + calendarDays() { + return this.getFullWeeksStartAndEndInMonth() + .map((week) => this.renderWeek(week)) + .join(""); + } + + renderWeek(week) { + const days = week + .map((day) => { + return this.renderDay(day); + }) + .join(""); + return `${days}`; + } + + renderDay(day) { + const today = new Date(); + let dateHTML = ""; + const data = { day: day, dayDate: day.getDate() }; + + if (day.toDateString() === this.selectedDate().toDateString()) { + // selectedDate + // Render the selected date template target innerHTML with Mustache + dateHTML = Mustache.render( + this.selectedDateTemplateTarget.innerHTML, + data, + ); + } else if (day.toDateString() === today.toDateString()) { + // todayDate + dateHTML = Mustache.render(this.todayDateTemplateTarget.innerHTML, data); + } else if (day.getMonth() === this.viewDate().getMonth()) { + // currentMonthDate + dateHTML = Mustache.render( + this.currentMonthDateTemplateTarget.innerHTML, + data, + ); + } else { + // otherMonthDate + dateHTML = Mustache.render( + this.otherMonthDateTemplateTarget.innerHTML, + data, + ); + } + return dateHTML; + } + + monthAndYear() { + const month = this.viewDate().toLocaleString("en-US", { month: "long" }); + const year = this.viewDate().getFullYear(); + return `${month} ${year}`; + } + + selectedDate() { + return new Date(this.selectedDateValue); + } + + viewDate() { + return this.viewDateValue + ? new Date(this.viewDateValue) + : this.selectedDate(); + } + + getFullWeeksStartAndEndInMonth() { + const month = this.viewDate().getMonth(); + const year = this.viewDate().getFullYear(); + + let weeks = [], + firstDate = new Date(year, month, 1), + lastDate = new Date(year, month + 1, 0), + numDays = lastDate.getDate(); + + let start = 1; + let end; + if (firstDate.getDay() === 1) { + end = 7; + } else if (firstDate.getDay() === 0) { + let preMonthEndDay = new Date(year, month, 0); + start = preMonthEndDay.getDate() - 6 + 1; + end = 1; + } else { + let preMonthEndDay = new Date(year, month, 0); + start = preMonthEndDay.getDate() + 1 - firstDate.getDay() + 1; + end = 7 - firstDate.getDay() + 1; + weeks.push({ + start: start, + end: end, + }); + start = end + 1; + end = end + 7; + } + while (start <= numDays) { + weeks.push({ + start: start, + end: end, + }); + start = end + 1; + end = end + 7; + end = start === 1 && end === 8 ? 1 : end; + if (end > numDays && start <= numDays) { + end = end - numDays; + weeks.push({ + start: start, + end: end, + }); + break; + } + } + // *** the magic starts here + return weeks.map(({ start, end }, index) => { + const sub = +(start > end && index === 0); + return Array.from({ length: 7 }, (_, index) => { + const date = new Date(year, month - sub, start + index); + return date; + }); + }); + } + + formatDate(date) { + const format = this.formatValue; + const day = date.getDate(); + const month = date.getMonth() + 1; + const year = date.getFullYear(); + const hours = date.getHours(); + const minutes = date.getMinutes(); + const seconds = date.getSeconds(); + const dayOfWeek = date.toLocaleString("en-US", { weekday: "long" }); + const monthName = date.toLocaleString("en-US", { month: "long" }); + const daySuffix = this.getDaySuffix(day); + + const map = { + yyyy: year, + MM: ("0" + month).slice(-2), + dd: ("0" + day).slice(-2), + HH: ("0" + hours).slice(-2), + mm: ("0" + minutes).slice(-2), + ss: ("0" + seconds).slice(-2), + EEEE: dayOfWeek, + MMMM: monthName, + do: day + daySuffix, + PPPP: `${dayOfWeek}, ${monthName} ${day}${daySuffix}, ${year}`, + }; + + const formattedDate = format.replace( + /yyyy|MM|dd|HH|mm|ss|EEEE|MMMM|do|PPPP/g, + (matched) => map[matched], + ); + return formattedDate; + } + + getDaySuffix(day) { + if (day > 3 && day < 21) return "th"; + switch (day % 10) { + case 1: + return "st"; + case 2: + return "nd"; + case 3: + return "rd"; + default: + return "th"; + } + } +} diff --git a/lib/ruby_ui/calendar/calendar_days.rb b/gem/lib/ruby_ui/calendar/calendar_days.rb similarity index 100% rename from lib/ruby_ui/calendar/calendar_days.rb rename to gem/lib/ruby_ui/calendar/calendar_days.rb diff --git a/gem/lib/ruby_ui/calendar/calendar_docs.rb b/gem/lib/ruby_ui/calendar/calendar_docs.rb new file mode 100644 index 00000000..c8f9a519 --- /dev/null +++ b/gem/lib/ruby_ui/calendar/calendar_docs.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class Views::Docs::Calendar < Views::Base + def view_template + component = "Calendar" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Calendar", description: "A date field component that allows users to enter and edit date.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Connect to input", context: self) do + <<~RUBY + div(class: 'space-y-4') do + Input(type: 'string', placeholder: "Select a date", class: 'rounded-md border shadow', id: 'date', data_controller: 'ruby-ui--calendar-input') + Calendar(input_id: '#date', class: 'rounded-md border shadow') + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Format date", description: "Format dates with date-fns", context: self) do + <<~RUBY + div(class: 'space-y-4') do + Input(type: 'string', placeholder: "Select a date", class: 'rounded-md border shadow', id: 'formatted-date', data_controller: 'ruby-ui--calendar-input') + Calendar(input_id: '#formatted-date', date_format: 'PPPP', class: 'rounded-md border shadow') + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/calendar/calendar_header.rb b/gem/lib/ruby_ui/calendar/calendar_header.rb similarity index 100% rename from lib/ruby_ui/calendar/calendar_header.rb rename to gem/lib/ruby_ui/calendar/calendar_header.rb diff --git a/gem/lib/ruby_ui/calendar/calendar_input_controller.js b/gem/lib/ruby_ui/calendar/calendar_input_controller.js new file mode 100644 index 00000000..17b48adc --- /dev/null +++ b/gem/lib/ruby_ui/calendar/calendar_input_controller.js @@ -0,0 +1,8 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="input" +export default class extends Controller { + setValue(value) { + this.element.value = value + } +} diff --git a/lib/ruby_ui/calendar/calendar_next.rb b/gem/lib/ruby_ui/calendar/calendar_next.rb similarity index 100% rename from lib/ruby_ui/calendar/calendar_next.rb rename to gem/lib/ruby_ui/calendar/calendar_next.rb diff --git a/lib/ruby_ui/calendar/calendar_prev.rb b/gem/lib/ruby_ui/calendar/calendar_prev.rb similarity index 100% rename from lib/ruby_ui/calendar/calendar_prev.rb rename to gem/lib/ruby_ui/calendar/calendar_prev.rb diff --git a/lib/ruby_ui/calendar/calendar_title.rb b/gem/lib/ruby_ui/calendar/calendar_title.rb similarity index 100% rename from lib/ruby_ui/calendar/calendar_title.rb rename to gem/lib/ruby_ui/calendar/calendar_title.rb diff --git a/lib/ruby_ui/calendar/calendar_weekdays.rb b/gem/lib/ruby_ui/calendar/calendar_weekdays.rb similarity index 100% rename from lib/ruby_ui/calendar/calendar_weekdays.rb rename to gem/lib/ruby_ui/calendar/calendar_weekdays.rb diff --git a/lib/ruby_ui/card/card.rb b/gem/lib/ruby_ui/card/card.rb similarity index 100% rename from lib/ruby_ui/card/card.rb rename to gem/lib/ruby_ui/card/card.rb diff --git a/lib/ruby_ui/card/card_content.rb b/gem/lib/ruby_ui/card/card_content.rb similarity index 100% rename from lib/ruby_ui/card/card_content.rb rename to gem/lib/ruby_ui/card/card_content.rb diff --git a/lib/ruby_ui/card/card_description.rb b/gem/lib/ruby_ui/card/card_description.rb similarity index 100% rename from lib/ruby_ui/card/card_description.rb rename to gem/lib/ruby_ui/card/card_description.rb diff --git a/gem/lib/ruby_ui/card/card_docs.rb b/gem/lib/ruby_ui/card/card_docs.rb new file mode 100644 index 00000000..b0ac30df --- /dev/null +++ b/gem/lib/ruby_ui/card/card_docs.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +class Views::Docs::Card < Views::Base + def view_template + component = "Card" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Card", description: "Displays a card with header, content, and footer.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Card with image", context: self) do + <<~RUBY + Card(class: 'w-96') do + CardHeader do + CardTitle { 'You might like "RubyUI"' } + CardDescription { "@joeldrapper" } + end + CardContent do + AspectRatio(aspect_ratio: "16/9", class: "rounded-md overflow-hidden border") do + img( + alt: "Placeholder", + loading: "lazy", + src: image_url('pattern.jpg') + ) + end + end + CardFooter(class: 'flex justify-end gap-x-2') do + Button(variant: :outline) { "See more" } + Button(variant: :primary) { "Buy now" } + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Card with full-width image", context: self) do + <<~RUBY + Card(class: 'w-96 overflow-hidden') do + AspectRatio(aspect_ratio: "16/9", class: "border-b") do + img( + alt: "Placeholder", + loading: "lazy", + src: image_url('pattern.jpg') + ) + end + CardHeader do + CardTitle { 'Introducing RubyUI' } + CardDescription { "Kickstart your project today!" } + end + CardFooter(class: 'flex justify-end') do + Button(variant: :outline) { "Get started" } + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Account balance", context: self) do + <<~RUBY + Card(class: 'w-96 overflow-hidden') do + CardHeader do + div(class: 'w-10 h-10 rounded-xl flex items-center justify-center bg-violet-100 text-violet-700 -rotate-6') do + cash_icon + end + end + CardContent(class: 'space-y-1') do + CardDescription(class: 'font-medium') { "Current Balance" } + h5(class: 'font-semibold text-4xl') { '$2,602' } + end + CardFooter do + Text(size: "2", class: "text-muted-foreground") { "**** 4620" } + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end + + def arrow_icon(classes: nil) + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 20 20", + fill: "currentColor", + class: ["w-4 h-4", classes] + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M3 10a.75.75 0 01.75-.75h10.638L10.23 5.29a.75.75 0 111.04-1.08l5.5 5.25a.75.75 0 010 1.08l-5.5 5.25a.75.75 0 11-1.04-1.08l4.158-3.96H3.75A.75.75 0 013 10z", + clip_rule: "evenodd" + ) + end + end + + def cash_icon(classes: nil) + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: ["w-6 h-6", classes] + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z" + ) + end + end +end diff --git a/lib/ruby_ui/card/card_footer.rb b/gem/lib/ruby_ui/card/card_footer.rb similarity index 100% rename from lib/ruby_ui/card/card_footer.rb rename to gem/lib/ruby_ui/card/card_footer.rb diff --git a/lib/ruby_ui/card/card_header.rb b/gem/lib/ruby_ui/card/card_header.rb similarity index 100% rename from lib/ruby_ui/card/card_header.rb rename to gem/lib/ruby_ui/card/card_header.rb diff --git a/lib/ruby_ui/card/card_title.rb b/gem/lib/ruby_ui/card/card_title.rb similarity index 100% rename from lib/ruby_ui/card/card_title.rb rename to gem/lib/ruby_ui/card/card_title.rb diff --git a/lib/ruby_ui/carousel/carousel.rb b/gem/lib/ruby_ui/carousel/carousel.rb similarity index 100% rename from lib/ruby_ui/carousel/carousel.rb rename to gem/lib/ruby_ui/carousel/carousel.rb diff --git a/lib/ruby_ui/carousel/carousel_content.rb b/gem/lib/ruby_ui/carousel/carousel_content.rb similarity index 100% rename from lib/ruby_ui/carousel/carousel_content.rb rename to gem/lib/ruby_ui/carousel/carousel_content.rb diff --git a/gem/lib/ruby_ui/carousel/carousel_controller.js b/gem/lib/ruby_ui/carousel/carousel_controller.js new file mode 100644 index 00000000..cc7402c5 --- /dev/null +++ b/gem/lib/ruby_ui/carousel/carousel_controller.js @@ -0,0 +1,60 @@ +import { Controller } from "@hotwired/stimulus"; +import EmblaCarousel from 'embla-carousel' + +const DEFAULT_OPTIONS = { + loop: true +} + +export default class extends Controller { + static values = { + options: { + type: Object, + default: {}, + } + } + static targets = ["viewport", "nextButton", "prevButton"] + + connect() { + this.initCarousel(this.#mergedOptions) + } + + disconnect() { + this.destroyCarousel() + } + + initCarousel(options, plugins = []) { + this.carousel = EmblaCarousel(this.viewportTarget, options, plugins) + + this.carousel.on("init", this.#updateControls.bind(this)) + this.carousel.on("reInit", this.#updateControls.bind(this)) + this.carousel.on("select", this.#updateControls.bind(this)) + } + + destroyCarousel() { + this.carousel.destroy() + } + + scrollNext() { + this.carousel.scrollNext() + } + + scrollPrev() { + this.carousel.scrollPrev() + } + + #updateControls() { + this.#toggleButtonsDisabledState(this.nextButtonTargets, !this.carousel.canScrollNext()) + this.#toggleButtonsDisabledState(this.prevButtonTargets, !this.carousel.canScrollPrev()) + } + + #toggleButtonsDisabledState(buttons, isDisabled) { + buttons.forEach((button) => button.disabled = isDisabled) + } + + get #mergedOptions() { + return { + ...DEFAULT_OPTIONS, + ...this.optionsValue + } + } +} diff --git a/gem/lib/ruby_ui/carousel/carousel_docs.rb b/gem/lib/ruby_ui/carousel/carousel_docs.rb new file mode 100644 index 00000000..7a11402d --- /dev/null +++ b/gem/lib/ruby_ui/carousel/carousel_docs.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +class Views::Docs::Carousel < Views::Base + def view_template + component = "Carousel" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Carousel", description: "A carousel with motion and swipe built using Embla.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + Carousel(options: {loop:false}, class: "w-full max-w-xs") do + CarouselContent do + 5.times do |index| + CarouselItem do + div(class: "p-1") do + Card do + CardContent(class: "flex aspect-square items-center justify-center p-6") do + span(class: "text-4xl font-semibold") { index + 1 } + end + end + end + end + end + end + CarouselPrevious() + CarouselNext() + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Sizes", context: self) do + <<~RUBY + Carousel(class: "w-full max-w-sm") do + CarouselContent do + 5.times do |index| + CarouselItem(class: "md:basis-1/2 lg:basis-1/3") do + div(class: "p-1") do + Card do + CardContent(class: "flex aspect-square items-center justify-center p-6") do + span(class: "text-3xl font-semibold") { index + 1 } + end + end + end + end + end + end + CarouselPrevious() + CarouselNext() + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Spacing", context: self) do + <<~RUBY + Carousel(class: "w-full max-w-sm") do + CarouselContent(class: "-ml-1") do + 5.times do |index| + CarouselItem(class: "pl-1 md:basis-1/2 lg:basis-1/3") do + div(class: "p-1") do + Card do + CardContent(class: "flex aspect-square items-center justify-center p-6") do + span(class: "text-2xl font-semibold") { index + 1 } + end + end + end + end + end + end + CarouselPrevious() + CarouselNext() + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Orientation", context: self) do + <<~RUBY + Carousel(orientation: :vertical, options: {align: "start"}, class: "w-full max-w-xs") do + CarouselContent(class: "-mt-1 h-[200px]") do + 5.times do |index| + CarouselItem(class: "pt-1 md:basis-1/2") do + div(class: "p-1") do + Card do + CardContent(class: "flex items-center justify-center p-6") do + span(class: "text-3xl font-semibold") { index + 1 } + end + end + end + end + end + end + CarouselPrevious() + CarouselNext() + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/carousel/carousel_item.rb b/gem/lib/ruby_ui/carousel/carousel_item.rb similarity index 100% rename from lib/ruby_ui/carousel/carousel_item.rb rename to gem/lib/ruby_ui/carousel/carousel_item.rb diff --git a/lib/ruby_ui/carousel/carousel_next.rb b/gem/lib/ruby_ui/carousel/carousel_next.rb similarity index 100% rename from lib/ruby_ui/carousel/carousel_next.rb rename to gem/lib/ruby_ui/carousel/carousel_next.rb diff --git a/lib/ruby_ui/carousel/carousel_previous.rb b/gem/lib/ruby_ui/carousel/carousel_previous.rb similarity index 100% rename from lib/ruby_ui/carousel/carousel_previous.rb rename to gem/lib/ruby_ui/carousel/carousel_previous.rb diff --git a/lib/ruby_ui/chart/chart.rb b/gem/lib/ruby_ui/chart/chart.rb similarity index 100% rename from lib/ruby_ui/chart/chart.rb rename to gem/lib/ruby_ui/chart/chart.rb diff --git a/gem/lib/ruby_ui/chart/chart_controller.js b/gem/lib/ruby_ui/chart/chart_controller.js new file mode 100644 index 00000000..18034447 --- /dev/null +++ b/gem/lib/ruby_ui/chart/chart_controller.js @@ -0,0 +1,103 @@ +import { Controller } from "@hotwired/stimulus" +import Chart from 'chart.js/auto' + +// Chart controller +export default class extends Controller { + static values = { + options: { + type: Object, + default: {}, + } + } + + // Function to initialize the chart when the controller is connected + connect() { + this.initDarkModeObserver() + this.initChart() + } + + disconnect() { + this.darkModeObserver?.disconnect() + this.chart?.destroy() + } + + // Function to initialize the chart + initChart() { + this.setColors() + const ctx = this.element.getContext('2d'); + this.chart = new Chart(ctx, this.mergeOptionsWithDefaults()); + } + + setColors() { + this.setDefaultColorsForChart() + } + + getThemeColor(name) { + const color = getComputedStyle(document.documentElement).getPropertyValue(`--${name}`) + const [hue, saturation, lightness] = color.split(' ') + return `hsl(${hue}, ${saturation}, ${lightness})` + } + + defaultThemeColor() { + return { + backgroundColor: this.getThemeColor('background'), + hoverBackgroundColor: this.getThemeColor('accent'), + borderColor: this.getThemeColor('primary'), + borderWidth: 1, + } + } + + // Function to set chart default colors + setDefaultColorsForChart() { + Chart.defaults.color = this.getThemeColor('muted-foreground') // font color + Chart.defaults.borderColor = this.getThemeColor('border') // border color + Chart.defaults.backgroundColor = this.getThemeColor('background') // background color + + // tooltip colors + Chart.defaults.plugins.tooltip.backgroundColor = this.getThemeColor('background') + Chart.defaults.plugins.tooltip.borderColor = this.getThemeColor('border') + Chart.defaults.plugins.tooltip.titleColor = this.getThemeColor('foreground') + Chart.defaults.plugins.tooltip.bodyColor = this.getThemeColor('muted-foreground') + Chart.defaults.plugins.tooltip.borderWidth = 1 + + // legend + // options.plugins.legend.labels + Chart.defaults.plugins.legend.labels.boxWidth = 12 + Chart.defaults.plugins.legend.labels.boxHeight = 12 + Chart.defaults.plugins.legend.labels.borderWidth = 0 + Chart.defaults.plugins.legend.labels.useBorderRadius = true + Chart.defaults.plugins.legend.labels.borderRadius = this.getThemeColor('radius') + } + + // Function to refresh the chart + refreshChart() { + // Destroy the chart if it's a valid Chart.js instance + this.chart?.destroy() + // Reinitialize the chart + this.initChart() + } + + // Function to initialize the dark mode observer + initDarkModeObserver() { + this.darkModeObserver = new MutationObserver(() => { + this.refreshChart() + }) + this.darkModeObserver.observe(document.documentElement, { attributeFilter: ['class'] }) + } + + // Function to merge the options with the defaults + mergeOptionsWithDefaults() { + return { + ...this.optionsValue, + data: { + ...this.optionsValue.data, + datasets: this.optionsValue.data.datasets.map((dataset) => { + return { + ...this.defaultThemeColor(), + ...dataset, + } + }) + } + } + } +} diff --git a/lib/ruby_ui/chart/chart_docs.rb b/gem/lib/ruby_ui/chart/chart_docs.rb similarity index 100% rename from lib/ruby_ui/chart/chart_docs.rb rename to gem/lib/ruby_ui/chart/chart_docs.rb diff --git a/lib/ruby_ui/checkbox/checkbox.rb b/gem/lib/ruby_ui/checkbox/checkbox.rb similarity index 100% rename from lib/ruby_ui/checkbox/checkbox.rb rename to gem/lib/ruby_ui/checkbox/checkbox.rb diff --git a/gem/lib/ruby_ui/checkbox/checkbox_docs.rb b/gem/lib/ruby_ui/checkbox/checkbox_docs.rb new file mode 100644 index 00000000..941ccc1c --- /dev/null +++ b/gem/lib/ruby_ui/checkbox/checkbox_docs.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class Views::Docs::Checkbox < Views::Base + def view_template + component = "Checkbox" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Checkbox", description: "A control that allows the user to toggle between checked and not checked.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + div(class: 'flex items-center space-x-3') do + Checkbox(id: 'terms') + label(for: 'terms', class: 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70') { "Accept terms and conditions" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Checked", context: self) do + <<~RUBY + div(class: "items-top flex space-x-3") do + Checkbox(id: 'terms1', checked: true) + div(class: "grid gap-1.5 leading-none") do + label( + for: "terms1", + class: + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" + ) { " Accept terms and conditions " } + p(class: "text-sm text-muted-foreground") { " You agree to our Terms of Service and Privacy Policy." } + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/checkbox/checkbox_group.rb b/gem/lib/ruby_ui/checkbox/checkbox_group.rb similarity index 100% rename from lib/ruby_ui/checkbox/checkbox_group.rb rename to gem/lib/ruby_ui/checkbox/checkbox_group.rb diff --git a/gem/lib/ruby_ui/checkbox/checkbox_group_controller.js b/gem/lib/ruby_ui/checkbox/checkbox_group_controller.js new file mode 100644 index 00000000..546167ad --- /dev/null +++ b/gem/lib/ruby_ui/checkbox/checkbox_group_controller.js @@ -0,0 +1,21 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = ["checkbox"]; + + connect() { + this.#handleRequired(); + } + + onChange() { + this.#handleRequired(); + } + + #handleRequired() { + if (!this.element.hasAttribute("data-required")) return; + + const checked = this.checkboxTargets.some(({ checked }) => checked); + + this.checkboxTargets.forEach((checkbox) => (checkbox.required = !checked)); + } +} diff --git a/lib/ruby_ui/clipboard/clipboard.rb b/gem/lib/ruby_ui/clipboard/clipboard.rb similarity index 100% rename from lib/ruby_ui/clipboard/clipboard.rb rename to gem/lib/ruby_ui/clipboard/clipboard.rb diff --git a/gem/lib/ruby_ui/clipboard/clipboard_controller.js b/gem/lib/ruby_ui/clipboard/clipboard_controller.js new file mode 100644 index 00000000..00c6f5b3 --- /dev/null +++ b/gem/lib/ruby_ui/clipboard/clipboard_controller.js @@ -0,0 +1,54 @@ +import { Controller } from "@hotwired/stimulus" +import { computePosition, flip, shift } from "@floating-ui/dom"; + +// Connects to data-controller="accordion" +export default class extends Controller { + static targets = ['trigger', 'source', 'successPopover', 'errorPopover'] + static values = { + options: { + type: Object, + default: {}, + }, + } + + copy() { + let sourceElement = this.sourceTarget.children[0]; + if (!sourceElement) { + this.showErrorPopover(); + return; + } + let textToCopy = sourceElement.tagName === 'INPUT' ? sourceElement.value : sourceElement.innerText; + navigator.clipboard.writeText(textToCopy).then(() => { + this.#showSuccessPopover(); + }).catch(() => { + this.#showErrorPopover(); + }) + } + + onClickOutside() { + if (!this.successPopoverTarget.classList.contains("hidden")) this.successPopoverTarget.classList.add("hidden"); + if (!this.errorPopoverTarget.classList.contains("hidden")) this.errorPopoverTarget.classList.add("hidden"); + } + + #computeTooltip(popoverElement) { + computePosition(this.triggerTarget, popoverElement, { + placement: this.optionsValue.placement || "top", + middleware: [flip(), shift()], + }).then(({ x, y }) => { + Object.assign(popoverElement.style, { + left: `${x}px`, + top: `${y}px`, + }); + }); + } + + #showSuccessPopover() { + this.#computeTooltip(this.successPopoverTarget); + this.successPopoverTarget.classList.remove("hidden"); + } + + #showErrorPopover() { + this.#computeTooltip(this.errorPopoverTarget); + this.errorPopoverTarget.classList.remove("hidden"); + } +} diff --git a/gem/lib/ruby_ui/clipboard/clipboard_docs.rb b/gem/lib/ruby_ui/clipboard/clipboard_docs.rb new file mode 100644 index 00000000..c6075d06 --- /dev/null +++ b/gem/lib/ruby_ui/clipboard/clipboard_docs.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class Views::Docs::Clipboard < Views::Base + def view_template + component = "Clipboard" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Clipboard", description: "A control to allow you to copy content to the clipboard.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + Clipboard(success: "Copied!", error: "Copy failed!", class: "relative", options: {placement: "top"}) do + ClipboardSource(class: "hidden") { span { "Born rich!!!" } } + + ClipboardTrigger do + Link(href: "#", class: "gap-1") do + Text(size: :small, class: "text-primary") { "Copy the secret of success!!!" } + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/clipboard/clipboard_popover.rb b/gem/lib/ruby_ui/clipboard/clipboard_popover.rb similarity index 100% rename from lib/ruby_ui/clipboard/clipboard_popover.rb rename to gem/lib/ruby_ui/clipboard/clipboard_popover.rb diff --git a/lib/ruby_ui/clipboard/clipboard_source.rb b/gem/lib/ruby_ui/clipboard/clipboard_source.rb similarity index 100% rename from lib/ruby_ui/clipboard/clipboard_source.rb rename to gem/lib/ruby_ui/clipboard/clipboard_source.rb diff --git a/lib/ruby_ui/clipboard/clipboard_trigger.rb b/gem/lib/ruby_ui/clipboard/clipboard_trigger.rb similarity index 100% rename from lib/ruby_ui/clipboard/clipboard_trigger.rb rename to gem/lib/ruby_ui/clipboard/clipboard_trigger.rb diff --git a/lib/ruby_ui/codeblock/codeblock.rb b/gem/lib/ruby_ui/codeblock/codeblock.rb similarity index 100% rename from lib/ruby_ui/codeblock/codeblock.rb rename to gem/lib/ruby_ui/codeblock/codeblock.rb diff --git a/gem/lib/ruby_ui/codeblock/codeblock_docs.rb b/gem/lib/ruby_ui/codeblock/codeblock_docs.rb new file mode 100644 index 00000000..23f1be61 --- /dev/null +++ b/gem/lib/ruby_ui/codeblock/codeblock_docs.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +class Views::Docs::Codeblock < Views::Base + def view_template + component = "Codeblock" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Codeblock", description: "A component for displaying highlighted code.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "With clipboard", context: self) do + <<~RUBY + code = <<~CODE + def hello_world + puts "Hello, world!" + end + CODE + div(class: 'w-full') do + Codeblock(code, syntax: :ruby) + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Without clipboard", context: self) do + <<~RUBY + code = <<~CODE + def hello_world + puts "Hello, world!" + end + CODE + div(class: 'w-full') do + Codeblock(code, syntax: :ruby, clipboard: false) + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Custom message", description: "Copy the code to see the message", context: self) do + <<~RUBY + code = <<~CODE + def hello_world + puts "Hello, world!" + end + CODE + div(class: 'w-full') do + Codeblock(code, syntax: :ruby, clipboard_success: "Nice one!") + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/collapsible/collapsible.rb b/gem/lib/ruby_ui/collapsible/collapsible.rb similarity index 100% rename from lib/ruby_ui/collapsible/collapsible.rb rename to gem/lib/ruby_ui/collapsible/collapsible.rb diff --git a/lib/ruby_ui/collapsible/collapsible_content.rb b/gem/lib/ruby_ui/collapsible/collapsible_content.rb similarity index 100% rename from lib/ruby_ui/collapsible/collapsible_content.rb rename to gem/lib/ruby_ui/collapsible/collapsible_content.rb diff --git a/gem/lib/ruby_ui/collapsible/collapsible_controller.js b/gem/lib/ruby_ui/collapsible/collapsible_controller.js new file mode 100644 index 00000000..cb367da3 --- /dev/null +++ b/gem/lib/ruby_ui/collapsible/collapsible_controller.js @@ -0,0 +1,47 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="accordion" +export default class extends Controller { + static targets = ['content'] + static values = { + open: { + type: Boolean, + default: false, + }, + } + + connect() { + // Set the initial state of the accordion + this.openValue ? this.open() : this.close() + } + + // Toggle the 'open' value + toggle() { + this.openValue = !this.openValue + } + + // Handle changes in the 'open' value + openValueChanged(isOpen, wasOpen) { + if (isOpen) { + this.open() + } else { + this.close() + } + } + + // Open the accordion content + open() { + if (this.hasContentTarget) { + this.contentTarget.classList.remove('hidden') + this.openValue = true + } + } + + // Close the accordion content + close() { + if (this.hasContentTarget) { + this.contentTarget.classList.add('hidden') + this.openValue = false + } + } +} diff --git a/gem/lib/ruby_ui/collapsible/collapsible_docs.rb b/gem/lib/ruby_ui/collapsible/collapsible_docs.rb new file mode 100644 index 00000000..04654686 --- /dev/null +++ b/gem/lib/ruby_ui/collapsible/collapsible_docs.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +class Views::Docs::Collapsible < Views::Base + def view_template + component = "Collapsible" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Collapsible", description: "An interactive component which expands/collapses a panel.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + Collapsible do + div(class: "flex items-center justify-between space-x-4 px-4 py-2") do + h4(class: "text-sm font-semibold") { " @joeldrapper starred 3 repositories" } + CollapsibleTrigger do + Button(variant: :ghost, icon: true) do + chevron_icon + span(class: "sr-only") { "Toggle" } + end + end + end + + div(class: "rounded-md border px-4 py-2 font-mono text-sm shadow-sm") do + "phlex-ruby/phlex" + end + + CollapsibleContent do + div(class: 'space-y-2 mt-2') do + div(class: "rounded-md border px-4 py-2 font-mono text-sm shadow-sm") do + "phlex-ruby/phlex-rails" + end + div(class: "rounded-md border px-4 py-2 font-mono text-sm shadow-sm") do + "ruby-ui/ruby_ui" + end + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Open", context: self) do + <<~RUBY + Collapsible(open: true) do + div(class: "flex items-center justify-between space-x-4 px-4 py-2") do + h4(class: "text-sm font-semibold") { " @joeldrapper starred 3 repositories" } + CollapsibleTrigger do + Button(variant: :ghost, icon: true) do + chevron_icon + span(class: "sr-only") { "Toggle" } + end + end + end + + div(class: "rounded-md border px-4 py-2 font-mono text-sm shadow-sm") do + "phlex-ruby/phlex" + end + + CollapsibleContent do + div(class: 'space-y-2 mt-2') do + div(class: "rounded-md border px-4 py-2 font-mono text-sm shadow-sm") do + "phlex-ruby/phlex-rails" + end + div(class: "rounded-md border px-4 py-2 font-mono text-sm shadow-sm") do + "ruby-ui/ruby_ui" + end + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end + + private + + def chevron_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 20 20", + fill: "currentColor", + class: "w-4 h-4" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z", + clip_rule: "evenodd" + ) + end + end +end diff --git a/lib/ruby_ui/collapsible/collapsible_trigger.rb b/gem/lib/ruby_ui/collapsible/collapsible_trigger.rb similarity index 100% rename from lib/ruby_ui/collapsible/collapsible_trigger.rb rename to gem/lib/ruby_ui/collapsible/collapsible_trigger.rb diff --git a/lib/ruby_ui/combobox/combobox.rb b/gem/lib/ruby_ui/combobox/combobox.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox.rb rename to gem/lib/ruby_ui/combobox/combobox.rb diff --git a/lib/ruby_ui/combobox/combobox_badge.rb b/gem/lib/ruby_ui/combobox/combobox_badge.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_badge.rb rename to gem/lib/ruby_ui/combobox/combobox_badge.rb diff --git a/lib/ruby_ui/combobox/combobox_badge_trigger.rb b/gem/lib/ruby_ui/combobox/combobox_badge_trigger.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_badge_trigger.rb rename to gem/lib/ruby_ui/combobox/combobox_badge_trigger.rb diff --git a/lib/ruby_ui/combobox/combobox_checkbox.rb b/gem/lib/ruby_ui/combobox/combobox_checkbox.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_checkbox.rb rename to gem/lib/ruby_ui/combobox/combobox_checkbox.rb diff --git a/lib/ruby_ui/combobox/combobox_clear_button.rb b/gem/lib/ruby_ui/combobox/combobox_clear_button.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_clear_button.rb rename to gem/lib/ruby_ui/combobox/combobox_clear_button.rb diff --git a/lib/ruby_ui/combobox/combobox_controller.js b/gem/lib/ruby_ui/combobox/combobox_controller.js similarity index 100% rename from lib/ruby_ui/combobox/combobox_controller.js rename to gem/lib/ruby_ui/combobox/combobox_controller.js diff --git a/lib/ruby_ui/combobox/combobox_docs.rb b/gem/lib/ruby_ui/combobox/combobox_docs.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_docs.rb rename to gem/lib/ruby_ui/combobox/combobox_docs.rb diff --git a/lib/ruby_ui/combobox/combobox_empty_state.rb b/gem/lib/ruby_ui/combobox/combobox_empty_state.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_empty_state.rb rename to gem/lib/ruby_ui/combobox/combobox_empty_state.rb diff --git a/lib/ruby_ui/combobox/combobox_input_trigger.rb b/gem/lib/ruby_ui/combobox/combobox_input_trigger.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_input_trigger.rb rename to gem/lib/ruby_ui/combobox/combobox_input_trigger.rb diff --git a/lib/ruby_ui/combobox/combobox_item.rb b/gem/lib/ruby_ui/combobox/combobox_item.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_item.rb rename to gem/lib/ruby_ui/combobox/combobox_item.rb diff --git a/lib/ruby_ui/combobox/combobox_item_indicator.rb b/gem/lib/ruby_ui/combobox/combobox_item_indicator.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_item_indicator.rb rename to gem/lib/ruby_ui/combobox/combobox_item_indicator.rb diff --git a/lib/ruby_ui/combobox/combobox_list.rb b/gem/lib/ruby_ui/combobox/combobox_list.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_list.rb rename to gem/lib/ruby_ui/combobox/combobox_list.rb diff --git a/lib/ruby_ui/combobox/combobox_list_group.rb b/gem/lib/ruby_ui/combobox/combobox_list_group.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_list_group.rb rename to gem/lib/ruby_ui/combobox/combobox_list_group.rb diff --git a/lib/ruby_ui/combobox/combobox_popover.rb b/gem/lib/ruby_ui/combobox/combobox_popover.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_popover.rb rename to gem/lib/ruby_ui/combobox/combobox_popover.rb diff --git a/lib/ruby_ui/combobox/combobox_radio.rb b/gem/lib/ruby_ui/combobox/combobox_radio.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_radio.rb rename to gem/lib/ruby_ui/combobox/combobox_radio.rb diff --git a/lib/ruby_ui/combobox/combobox_search_input.rb b/gem/lib/ruby_ui/combobox/combobox_search_input.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_search_input.rb rename to gem/lib/ruby_ui/combobox/combobox_search_input.rb diff --git a/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb b/gem/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb rename to gem/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb diff --git a/lib/ruby_ui/combobox/combobox_trigger.rb b/gem/lib/ruby_ui/combobox/combobox_trigger.rb similarity index 100% rename from lib/ruby_ui/combobox/combobox_trigger.rb rename to gem/lib/ruby_ui/combobox/combobox_trigger.rb diff --git a/lib/ruby_ui/command/command.rb b/gem/lib/ruby_ui/command/command.rb similarity index 100% rename from lib/ruby_ui/command/command.rb rename to gem/lib/ruby_ui/command/command.rb diff --git a/gem/lib/ruby_ui/command/command_controller.js b/gem/lib/ruby_ui/command/command_controller.js new file mode 100644 index 00000000..85c098ef --- /dev/null +++ b/gem/lib/ruby_ui/command/command_controller.js @@ -0,0 +1,135 @@ +import { Controller } from "@hotwired/stimulus"; +import Fuse from "fuse.js"; + +// Connects to data-controller="ruby-ui--command" +export default class extends Controller { + static targets = ["input", "group", "item", "empty", "content"]; + + static values = { + open: { + type: Boolean, + default: false, + }, + }; + + connect() { + this.inputTarget.focus(); + this.searchIndex = this.buildSearchIndex(); + this.toggleVisibility(this.emptyTargets, false); + this.selectedIndex = -1; + + if (this.openValue) { + this.open(); + } + } + + open(e) { + e.preventDefault(); + document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML); + // prevent scroll on body + document.body.classList.add("overflow-hidden"); + } + + dismiss() { + // allow scroll on body + document.body.classList.remove("overflow-hidden"); + // remove the element + this.element.remove(); + } + + filter(e) { + // Deselect any previously selected item + this.deselectAll(); + + const query = e.target.value.toLowerCase(); + if (query.length === 0) { + this.resetVisibility(); + return; + } + + this.toggleVisibility(this.itemTargets, false); + + const results = this.searchIndex.search(query); + results.forEach((result) => + this.toggleVisibility([result.item.element], true), + ); + + this.toggleVisibility(this.emptyTargets, results.length === 0); + this.updateGroupVisibility(); + } + + toggleVisibility(elements, isVisible) { + elements.forEach((el) => el.classList.toggle("hidden", !isVisible)); + } + + updateGroupVisibility() { + this.groupTargets.forEach((group) => { + const hasVisibleItems = + group.querySelectorAll( + "[data-ruby-ui--command-target='item']:not(.hidden)", + ).length > 0; + this.toggleVisibility([group], hasVisibleItems); + }); + } + + resetVisibility() { + this.toggleVisibility(this.itemTargets, true); + this.toggleVisibility(this.groupTargets, true); + this.toggleVisibility(this.emptyTargets, false); + } + + buildSearchIndex() { + const options = { + keys: ["value"], + threshold: 0.2, + includeMatches: true, + }; + const items = this.itemTargets.map((el) => ({ + value: el.dataset.value, + element: el, + })); + return new Fuse(items, options); + } + + handleKeydown(e) { + const visibleItems = this.itemTargets.filter( + (item) => !item.classList.contains("hidden"), + ); + if (e.key === "ArrowDown") { + e.preventDefault(); + this.updateSelectedItem(visibleItems, 1); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + this.updateSelectedItem(visibleItems, -1); + } else if (e.key === "Enter" && this.selectedIndex !== -1) { + e.preventDefault(); + visibleItems[this.selectedIndex].click(); + } + } + + updateSelectedItem(visibleItems, direction) { + if (this.selectedIndex >= 0) { + this.toggleAriaSelected(visibleItems[this.selectedIndex], false); + } + + this.selectedIndex += direction; + + // Ensure the selected index is within the bounds of the visible items + if (this.selectedIndex < 0) { + this.selectedIndex = visibleItems.length - 1; + } else if (this.selectedIndex >= visibleItems.length) { + this.selectedIndex = 0; + } + + this.toggleAriaSelected(visibleItems[this.selectedIndex], true); + } + + toggleAriaSelected(element, isSelected) { + element.setAttribute("aria-selected", isSelected.toString()); + } + + deselectAll() { + this.itemTargets.forEach((item) => this.toggleAriaSelected(item, false)); + this.selectedIndex = -1; + } +} diff --git a/lib/ruby_ui/command/command_dialog.rb b/gem/lib/ruby_ui/command/command_dialog.rb similarity index 100% rename from lib/ruby_ui/command/command_dialog.rb rename to gem/lib/ruby_ui/command/command_dialog.rb diff --git a/lib/ruby_ui/command/command_dialog_content.rb b/gem/lib/ruby_ui/command/command_dialog_content.rb similarity index 100% rename from lib/ruby_ui/command/command_dialog_content.rb rename to gem/lib/ruby_ui/command/command_dialog_content.rb diff --git a/lib/ruby_ui/command/command_dialog_trigger.rb b/gem/lib/ruby_ui/command/command_dialog_trigger.rb similarity index 100% rename from lib/ruby_ui/command/command_dialog_trigger.rb rename to gem/lib/ruby_ui/command/command_dialog_trigger.rb diff --git a/gem/lib/ruby_ui/command/command_docs.rb b/gem/lib/ruby_ui/command/command_docs.rb new file mode 100644 index 00000000..37ce24cb --- /dev/null +++ b/gem/lib/ruby_ui/command/command_docs.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +class Views::Docs::Command < Views::Base + def view_template + component = "Command" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Command", description: "Fast, composable, unstyled command menu for Phlex.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + CommandDialog do + CommandDialogTrigger do + Button(variant: "outline", class: 'w-56 pr-2 pl-3 justify-between') do + div(class: "flex items-center space-x-1") do + search_icon + span(class: "text-muted-foreground font-normal") do + plain "Search" + end + end + ShortcutKey do + span(class: "text-xs") { "⌘" } + plain "K" + end + end + end + CommandDialogContent do + Command do + CommandInput(placeholder: "Type a command or search...") + CommandEmpty { "No results found." } + CommandList do + CommandGroup(title: "Components") do + components_list.each do |component| + CommandItem(value: component[:name], href: component[:path]) do + default_icon + plain component[:name] + end + end + end + CommandGroup(title: "Settings") do + settings_list.each do |setting| + CommandItem(value: setting[:name], href: setting[:path]) do + default_icon + plain setting[:name] + end + end + end + end + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "With keybinding", context: self) do + <<~RUBY + CommandDialog do + CommandDialogTrigger(keybindings: ['keydown.ctrl+j@window', 'keydown.meta+j@window']) do + p(class: "text-sm text-muted-foreground") do + span(class: 'mr-1') { "Press" } + ShortcutKey do + span(class: "text-xs") { "⌘" } + plain "J" + end + end + end + CommandDialogContent do + Command do + CommandInput(placeholder: "Type a command or search...") + CommandEmpty { "No results found." } + CommandList do + CommandGroup(title: "Components") do + components_list.each do |component| + CommandItem(value: component[:name], href: component[:path]) do + default_icon + plain component[:name] + end + end + end + CommandGroup(title: "Settings") do + settings_list.each do |setting| + CommandItem(value: setting[:name], href: setting[:path]) do + default_icon + plain setting[:name] + end + end + end + end + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end + + private + + def search_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 20 20", + fill: "currentColor", + class: "w-4 h-4 mr-1.5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z", + clip_rule: "evenodd" + ) + end + end + + def default_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm4.28 10.28a.75.75 0 000-1.06l-3-3a.75.75 0 10-1.06 1.06l1.72 1.72H8.25a.75.75 0 000 1.5h5.69l-1.72 1.72a.75.75 0 101.06 1.06l3-3z", + clip_rule: "evenodd" + ) + end + end + + def components_list + [ + {name: "Accordion", path: docs_accordion_path}, + {name: "Alert", path: docs_alert_path}, + {name: "Alert Dialog", path: docs_alert_dialog_path}, + {name: "Aspect Ratio", path: docs_aspect_ratio_path}, + {name: "Avatar", path: docs_avatar_path}, + {name: "Badge", path: docs_badge_path} + ] + end + + def settings_list + [ + {name: "Profile", path: "#"}, + {name: "Mail", path: "#"}, + {name: "Settings", path: "#"} + ] + end +end diff --git a/lib/ruby_ui/command/command_empty.rb b/gem/lib/ruby_ui/command/command_empty.rb similarity index 100% rename from lib/ruby_ui/command/command_empty.rb rename to gem/lib/ruby_ui/command/command_empty.rb diff --git a/lib/ruby_ui/command/command_group.rb b/gem/lib/ruby_ui/command/command_group.rb similarity index 100% rename from lib/ruby_ui/command/command_group.rb rename to gem/lib/ruby_ui/command/command_group.rb diff --git a/lib/ruby_ui/command/command_input.rb b/gem/lib/ruby_ui/command/command_input.rb similarity index 100% rename from lib/ruby_ui/command/command_input.rb rename to gem/lib/ruby_ui/command/command_input.rb diff --git a/lib/ruby_ui/command/command_item.rb b/gem/lib/ruby_ui/command/command_item.rb similarity index 100% rename from lib/ruby_ui/command/command_item.rb rename to gem/lib/ruby_ui/command/command_item.rb diff --git a/lib/ruby_ui/command/command_list.rb b/gem/lib/ruby_ui/command/command_list.rb similarity index 100% rename from lib/ruby_ui/command/command_list.rb rename to gem/lib/ruby_ui/command/command_list.rb diff --git a/lib/ruby_ui/context_menu/context_menu.rb b/gem/lib/ruby_ui/context_menu/context_menu.rb similarity index 100% rename from lib/ruby_ui/context_menu/context_menu.rb rename to gem/lib/ruby_ui/context_menu/context_menu.rb diff --git a/lib/ruby_ui/context_menu/context_menu_content.rb b/gem/lib/ruby_ui/context_menu/context_menu_content.rb similarity index 100% rename from lib/ruby_ui/context_menu/context_menu_content.rb rename to gem/lib/ruby_ui/context_menu/context_menu_content.rb diff --git a/gem/lib/ruby_ui/context_menu/context_menu_controller.js b/gem/lib/ruby_ui/context_menu/context_menu_controller.js new file mode 100644 index 00000000..23510570 --- /dev/null +++ b/gem/lib/ruby_ui/context_menu/context_menu_controller.js @@ -0,0 +1,144 @@ +import { Controller } from "@hotwired/stimulus"; +import tippy from "tippy.js"; + +export default class extends Controller { + static targets = ["trigger", "content", "menuItem"]; + static values = { + options: { + type: Object, + default: {}, + }, + // make content width of the trigger element (true/false) + matchWidth: { + type: Boolean, + default: false, + } + } + + connect() { + this.boundHandleKeydown = this.handleKeydown.bind(this); // Bind the function so we can remove it later + this.initializeTippy(); + this.selectedIndex = -1; + } + + disconnect() { + this.destroyTippy(); + } + + initializeTippy() { + const defaultOptions = { + content: this.contentTarget.innerHTML, + allowHTML: true, + interactive: true, + onShow: (instance) => { + this.matchWidthValue && this.setContentWidth(instance); // ensure content width matches trigger width + this.addEventListeners(); + }, + onHide: () => { + this.removeEventListeners(); + this.deselectAll(); + }, + popperOptions: { + modifiers: [ + { + name: "offset", + options: { + offset: [0, 4] + }, + }, + ], + } + }; + + const mergedOptions = { ...this.optionsValue, ...defaultOptions }; + this.tippy = tippy(this.triggerTarget, mergedOptions); + } + + destroyTippy() { + if (this.tippy) { + this.tippy.destroy(); + } + } + + setContentWidth(instance) { + // box-sizing: border-box + const content = instance.popper.querySelector('.tippy-content'); + if (content) { + content.style.width = `${instance.reference.offsetWidth}px`; + } + } + + handleContextMenu(event) { + event.preventDefault(); + this.open(); + } + + open() { + this.tippy.show(); + } + + close() { + this.tippy.hide(); + } + + handleKeydown(e) { + // return if no menu items (one line fix for) + if (this.menuItemTargets.length === 0) { return; } + + if (e.key === 'ArrowDown') { + e.preventDefault(); + this.updateSelectedItem(1); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + this.updateSelectedItem(-1); + } else if (e.key === 'Enter' && this.selectedIndex !== -1) { + e.preventDefault(); + this.menuItemTargets[this.selectedIndex].click(); + } + } + + updateSelectedItem(direction) { + // Check if any of the menuItemTargets have aria-selected="true" and set the selectedIndex to that index + this.menuItemTargets.forEach((item, index) => { + if (item.getAttribute('aria-selected') === 'true') { + this.selectedIndex = index; + } + }); + + if (this.selectedIndex >= 0) { + this.toggleAriaSelected(this.menuItemTargets[this.selectedIndex], false); + } + + this.selectedIndex += direction; + + if (this.selectedIndex < 0) { + this.selectedIndex = this.menuItemTargets.length - 1; + } else if (this.selectedIndex >= this.menuItemTargets.length) { + this.selectedIndex = 0; + } + + this.toggleAriaSelected(this.menuItemTargets[this.selectedIndex], true); + } + + toggleAriaSelected(element, isSelected) { + // Add or remove attribute + if (isSelected) { + element.setAttribute('aria-selected', 'true'); + } else { + element.removeAttribute('aria-selected'); + } + } + + deselectAll() { + this.menuItemTargets.forEach(item => this.toggleAriaSelected(item, false)); + this.selectedIndex = -1; + } + + addEventListeners() { + document.addEventListener('keydown', this.boundHandleKeydown); + } + + removeEventListeners() { + document.removeEventListener('keydown', this.boundHandleKeydown); + } +} diff --git a/gem/lib/ruby_ui/context_menu/context_menu_docs.rb b/gem/lib/ruby_ui/context_menu/context_menu_docs.rb new file mode 100644 index 00000000..5043e21d --- /dev/null +++ b/gem/lib/ruby_ui/context_menu/context_menu_docs.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +class Views::Docs::ContextMenu < Views::Base + def view_template + component = "ContextMenu" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Context Menu", description: "Displays a menu to the user — such as a set of actions or functions — triggered by a right click.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + ContextMenu do + ContextMenuTrigger(class: 'flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed text-sm') { "Right click here" } + ContextMenuContent(class: 'w-64') do + ContextMenuItem(href: '#', shortcut: "⌘[") { "Back" } + ContextMenuItem(href: '#', shortcut: "⌘]", disabled: true) { "Forward" } + ContextMenuItem(href: '#', shortcut: "⌘R") { "Reload" } + ContextMenuSeparator + ContextMenuItem(href: '#', shortcut: "⌘⇧B", checked: true) { "Show Bookmarks Bar" } + ContextMenuItem(href: '#') { "Show Full URLs" } + ContextMenuSeparator + ContextMenuLabel(inset: true) { "More Tools" } + ContextMenuSeparator + ContextMenuItem(href: '#') { "Developer Tools" } + ContextMenuItem(href: '#') { "Task Manager" } + ContextMenuItem(href: '#') { "Extensions" } + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Placement", context: self) do + <<~RUBY + div(class: 'space-y-4') do + ContextMenu(options: { placement: 'right' }) do + ContextMenuTrigger(class: 'flex flex-col items-center gap-y-2 h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed text-sm') do + plain "Right click here" + Badge(variant: :primary) { "right" } + end + ContextMenuContent(class: 'w-64') do + ContextMenuItem(href: '#', shortcut: "⌘[") { "Back" } + ContextMenuItem(href: '#', shortcut: "⌘]", disabled: true) { "Forward" } + ContextMenuItem(href: '#', shortcut: "⌘R") { "Reload" } + ContextMenuSeparator + ContextMenuItem(href: '#', shortcut: "⌘⇧B", checked: true) { "Show Bookmarks Bar" } + ContextMenuItem(href: '#') { "Show Full URLs" } + ContextMenuSeparator + ContextMenuLabel(inset: true) { "More Tools" } + ContextMenuSeparator + ContextMenuItem(href: '#') { "Developer Tools" } + ContextMenuItem(href: '#') { "Task Manager" } + ContextMenuItem(href: '#') { "Extensions" } + end + end + ContextMenu(options: { placement: 'left' }) do + ContextMenuTrigger(class: 'flex flex-col items-center gap-y-2 h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed text-sm') do + plain "Right click here" + Badge(variant: :primary) { "left" } + end + ContextMenuContent(class: 'w-64') do + ContextMenuItem(href: '#', shortcut: "⌘[") { "Back" } + ContextMenuItem(href: '#', shortcut: "⌘]", disabled: true) { "Forward" } + ContextMenuItem(href: '#', shortcut: "⌘R") { "Reload" } + ContextMenuSeparator + ContextMenuItem(href: '#', shortcut: "⌘⇧B", checked: true) { "Show Bookmarks Bar" } + ContextMenuItem(href: '#') { "Show Full URLs" } + ContextMenuSeparator + ContextMenuLabel(inset: true) { "More Tools" } + ContextMenuSeparator + ContextMenuItem(href: '#') { "Developer Tools" } + ContextMenuItem(href: '#') { "Task Manager" } + ContextMenuItem(href: '#') { "Extensions" } + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/context_menu/context_menu_item.rb b/gem/lib/ruby_ui/context_menu/context_menu_item.rb similarity index 100% rename from lib/ruby_ui/context_menu/context_menu_item.rb rename to gem/lib/ruby_ui/context_menu/context_menu_item.rb diff --git a/lib/ruby_ui/context_menu/context_menu_label.rb b/gem/lib/ruby_ui/context_menu/context_menu_label.rb similarity index 100% rename from lib/ruby_ui/context_menu/context_menu_label.rb rename to gem/lib/ruby_ui/context_menu/context_menu_label.rb diff --git a/lib/ruby_ui/context_menu/context_menu_separator.rb b/gem/lib/ruby_ui/context_menu/context_menu_separator.rb similarity index 100% rename from lib/ruby_ui/context_menu/context_menu_separator.rb rename to gem/lib/ruby_ui/context_menu/context_menu_separator.rb diff --git a/lib/ruby_ui/context_menu/context_menu_trigger.rb b/gem/lib/ruby_ui/context_menu/context_menu_trigger.rb similarity index 100% rename from lib/ruby_ui/context_menu/context_menu_trigger.rb rename to gem/lib/ruby_ui/context_menu/context_menu_trigger.rb diff --git a/lib/ruby_ui/dialog/dialog.rb b/gem/lib/ruby_ui/dialog/dialog.rb similarity index 100% rename from lib/ruby_ui/dialog/dialog.rb rename to gem/lib/ruby_ui/dialog/dialog.rb diff --git a/lib/ruby_ui/dialog/dialog_content.rb b/gem/lib/ruby_ui/dialog/dialog_content.rb similarity index 100% rename from lib/ruby_ui/dialog/dialog_content.rb rename to gem/lib/ruby_ui/dialog/dialog_content.rb diff --git a/gem/lib/ruby_ui/dialog/dialog_controller.js b/gem/lib/ruby_ui/dialog/dialog_controller.js new file mode 100644 index 00000000..26bf1fa0 --- /dev/null +++ b/gem/lib/ruby_ui/dialog/dialog_controller.js @@ -0,0 +1,32 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="dialog" +export default class extends Controller { + static targets = ["content"] + static values = { + open: { + type: Boolean, + default: false + }, + } + + connect() { + if (this.openValue) { + this.open() + } + } + + open(e) { + e?.preventDefault(); + document.body.insertAdjacentHTML('beforeend', this.contentTarget.innerHTML) + // prevent scroll on body + document.body.classList.add('overflow-hidden') + } + + dismiss() { + // allow scroll on body + document.body.classList.remove('overflow-hidden') + // remove the element + this.element.remove() + } +} diff --git a/lib/ruby_ui/dialog/dialog_description.rb b/gem/lib/ruby_ui/dialog/dialog_description.rb similarity index 100% rename from lib/ruby_ui/dialog/dialog_description.rb rename to gem/lib/ruby_ui/dialog/dialog_description.rb diff --git a/gem/lib/ruby_ui/dialog/dialog_docs.rb b/gem/lib/ruby_ui/dialog/dialog_docs.rb new file mode 100644 index 00000000..9876cdd6 --- /dev/null +++ b/gem/lib/ruby_ui/dialog/dialog_docs.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +class Views::Docs::Dialog < Views::Base + def view_template + component = "Dialog" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Dialog", description: "A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + Dialog do + DialogTrigger do + Button { "Open Dialog" } + end + DialogContent do + DialogHeader do + DialogTitle { "RubyUI to the rescue" } + DialogDescription { "RubyUI helps you build accessible standard compliant web apps with ease" } + end + DialogMiddle do + AspectRatio(aspect_ratio: "16/9", class: 'rounded-md overflow-hidden border') do + img( + alt: "Placeholder", + loading: "lazy", + src: image_path("pattern.jpg") + ) + end + end + DialogFooter do + Button(variant: :outline, data: { action: 'click->ruby-ui--dialog#dismiss' }) { "Cancel" } + Button { "Save" } + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Size", description: "Applicable for wider screens", context: self) do + <<~RUBY + div(class: 'flex flex-wrap justify-center gap-2') do + Dialog do + DialogTrigger do + Button { "Small Dialog" } + end + DialogContent(size: :sm) do + DialogHeader do + DialogTitle { "RubyUI to the rescue" } + DialogDescription { "RubyUI helps you build accessible standard compliant web apps with ease" } + end + DialogMiddle do + AspectRatio(aspect_ratio: "16/9", class: 'rounded-md overflow-hidden border') do + img( + alt: "Placeholder", + loading: "lazy", + src: image_path("pattern.jpg") + ) + end + end + DialogFooter do + Button(variant: :outline, data: { action: 'click->ruby-ui--dialog#dismiss' }) { "Cancel" } + Button { "Save" } + end + end + end + + Dialog do + DialogTrigger do + Button { "Large Dialog" } + end + DialogContent(size: :lg) do + DialogHeader do + DialogTitle { "RubyUI to the rescue" } + DialogDescription { "RubyUI helps you build accessible standard compliant web apps with ease" } + end + DialogMiddle do + AspectRatio(aspect_ratio: "16/9", class: 'rounded-md overflow-hidden border') do + img( + alt: "Placeholder", + loading: "lazy", + src: image_path("pattern.jpg") + ) + end + end + DialogFooter do + Button(variant: :outline, data: { action: 'click->ruby-ui--dialog#dismiss' }) { "Cancel" } + Button { "Save" } + end + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/dialog/dialog_footer.rb b/gem/lib/ruby_ui/dialog/dialog_footer.rb similarity index 100% rename from lib/ruby_ui/dialog/dialog_footer.rb rename to gem/lib/ruby_ui/dialog/dialog_footer.rb diff --git a/lib/ruby_ui/dialog/dialog_header.rb b/gem/lib/ruby_ui/dialog/dialog_header.rb similarity index 100% rename from lib/ruby_ui/dialog/dialog_header.rb rename to gem/lib/ruby_ui/dialog/dialog_header.rb diff --git a/lib/ruby_ui/dialog/dialog_middle.rb b/gem/lib/ruby_ui/dialog/dialog_middle.rb similarity index 100% rename from lib/ruby_ui/dialog/dialog_middle.rb rename to gem/lib/ruby_ui/dialog/dialog_middle.rb diff --git a/lib/ruby_ui/dialog/dialog_title.rb b/gem/lib/ruby_ui/dialog/dialog_title.rb similarity index 100% rename from lib/ruby_ui/dialog/dialog_title.rb rename to gem/lib/ruby_ui/dialog/dialog_title.rb diff --git a/lib/ruby_ui/dialog/dialog_trigger.rb b/gem/lib/ruby_ui/dialog/dialog_trigger.rb similarity index 100% rename from lib/ruby_ui/dialog/dialog_trigger.rb rename to gem/lib/ruby_ui/dialog/dialog_trigger.rb diff --git a/lib/ruby_ui/docs/base.rb b/gem/lib/ruby_ui/docs/base.rb similarity index 100% rename from lib/ruby_ui/docs/base.rb rename to gem/lib/ruby_ui/docs/base.rb diff --git a/lib/ruby_ui/docs/component_setup_tabs.rb b/gem/lib/ruby_ui/docs/component_setup_tabs.rb similarity index 100% rename from lib/ruby_ui/docs/component_setup_tabs.rb rename to gem/lib/ruby_ui/docs/component_setup_tabs.rb diff --git a/lib/ruby_ui/docs/components_table.rb b/gem/lib/ruby_ui/docs/components_table.rb similarity index 100% rename from lib/ruby_ui/docs/components_table.rb rename to gem/lib/ruby_ui/docs/components_table.rb diff --git a/lib/ruby_ui/docs/header.rb b/gem/lib/ruby_ui/docs/header.rb similarity index 100% rename from lib/ruby_ui/docs/header.rb rename to gem/lib/ruby_ui/docs/header.rb diff --git a/lib/ruby_ui/docs/sidebar_examples.rb b/gem/lib/ruby_ui/docs/sidebar_examples.rb similarity index 100% rename from lib/ruby_ui/docs/sidebar_examples.rb rename to gem/lib/ruby_ui/docs/sidebar_examples.rb diff --git a/lib/ruby_ui/docs/visual_code_example.rb b/gem/lib/ruby_ui/docs/visual_code_example.rb similarity index 100% rename from lib/ruby_ui/docs/visual_code_example.rb rename to gem/lib/ruby_ui/docs/visual_code_example.rb diff --git a/lib/ruby_ui/dropdown_menu/dropdown_menu.rb b/gem/lib/ruby_ui/dropdown_menu/dropdown_menu.rb similarity index 100% rename from lib/ruby_ui/dropdown_menu/dropdown_menu.rb rename to gem/lib/ruby_ui/dropdown_menu/dropdown_menu.rb diff --git a/lib/ruby_ui/dropdown_menu/dropdown_menu_content.rb b/gem/lib/ruby_ui/dropdown_menu/dropdown_menu_content.rb similarity index 100% rename from lib/ruby_ui/dropdown_menu/dropdown_menu_content.rb rename to gem/lib/ruby_ui/dropdown_menu/dropdown_menu_content.rb diff --git a/gem/lib/ruby_ui/dropdown_menu/dropdown_menu_controller.js b/gem/lib/ruby_ui/dropdown_menu/dropdown_menu_controller.js new file mode 100644 index 00000000..d624eaf5 --- /dev/null +++ b/gem/lib/ruby_ui/dropdown_menu/dropdown_menu_controller.js @@ -0,0 +1,149 @@ +import { Controller } from "@hotwired/stimulus"; +import { + computePosition, + flip, + shift, + offset, + autoUpdate, +} from "@floating-ui/dom"; + +export default class extends Controller { + static targets = ["trigger", "content", "menuItem"]; + static values = { + open: { + type: Boolean, + default: false, + }, + options: { + type: Object, + default: {}, + }, + }; + + connect() { + this.boundHandleKeydown = this.#handleKeydown.bind(this); // Bind the function so we can remove it later + this.selectedIndex = -1; + + this.#setupAutoUpdate(); + } + + disconnect() { + if (this.autoUpdateCleanup) { + this.autoUpdateCleanup(); + } + } + + #setupAutoUpdate() { + this.autoUpdateCleanup = autoUpdate( + this.triggerTarget, + this.contentTarget, + this.#computeTooltip.bind(this), + ); + } + + #computeTooltip() { + computePosition(this.triggerTarget, this.contentTarget, { + placement: this.optionsValue.placement || "top", + middleware: [flip(), shift(), offset(8)], + strategy: this.optionsValue.strategy || "absolute", + }).then(({ x, y }) => { + Object.assign(this.contentTarget.style, { + left: `${x}px`, + top: `${y}px`, + }); + }); + } + + onClickOutside(event) { + if (!this.openValue) return; + if (this.element.contains(event.target)) return; + + event.preventDefault(); + this.close(); + } + + toggle() { + this.contentTarget.classList.contains("hidden") + ? this.#open() + : this.close(); + } + + #open() { + this.openValue = true; + this.#deselectAll(); + this.#addEventListeners(); + this.#computeTooltip(); + this.contentTarget.classList.remove("hidden"); + } + + close() { + this.openValue = false; + this.#removeEventListeners(); + this.contentTarget.classList.add("hidden"); + } + + #handleKeydown(e) { + // return if no menu items (one line fix for) + if (this.menuItemTargets.length === 0) { + return; + } + + if (e.key === "ArrowDown") { + e.preventDefault(); + this.#updateSelectedItem(1); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + this.#updateSelectedItem(-1); + } else if (e.key === "Enter" && this.selectedIndex !== -1) { + e.preventDefault(); + this.menuItemTargets[this.selectedIndex].click(); + } + } + + #updateSelectedItem(direction) { + // Check if any of the menuItemTargets have aria-selected="true" and set the selectedIndex to that index + this.menuItemTargets.forEach((item, index) => { + if (item.getAttribute("aria-selected") === "true") { + this.selectedIndex = index; + } + }); + + if (this.selectedIndex >= 0) { + this.#toggleAriaSelected(this.menuItemTargets[this.selectedIndex], false); + } + + this.selectedIndex += direction; + + if (this.selectedIndex < 0) { + this.selectedIndex = this.menuItemTargets.length - 1; + } else if (this.selectedIndex >= this.menuItemTargets.length) { + this.selectedIndex = 0; + } + + this.#toggleAriaSelected(this.menuItemTargets[this.selectedIndex], true); + } + + #toggleAriaSelected(element, isSelected) { + // Add or remove attribute + if (isSelected) { + element.setAttribute("aria-selected", "true"); + } else { + element.removeAttribute("aria-selected"); + } + } + + #deselectAll() { + this.menuItemTargets.forEach((item) => + this.#toggleAriaSelected(item, false), + ); + this.selectedIndex = -1; + } + + #addEventListeners() { + document.addEventListener("keydown", this.boundHandleKeydown); + } + + #removeEventListeners() { + document.removeEventListener("keydown", this.boundHandleKeydown); + } +} diff --git a/gem/lib/ruby_ui/dropdown_menu/dropdown_menu_docs.rb b/gem/lib/ruby_ui/dropdown_menu/dropdown_menu_docs.rb new file mode 100644 index 00000000..a57469e2 --- /dev/null +++ b/gem/lib/ruby_ui/dropdown_menu/dropdown_menu_docs.rb @@ -0,0 +1,212 @@ +# frozen_string_literal: true + +class Views::Docs::DropdownMenu < Views::Base + def view_template + component = "DropdownMenu" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Dropdown Menu", description: "Displays a menu to the user — such as a set of actions or functions — triggered by a button.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + DropdownMenu do + DropdownMenuTrigger(class: 'w-full') do + Button(variant: :outline) { "Open" } + end + DropdownMenuContent do + DropdownMenuLabel { "My Account" } + DropdownMenuSeparator + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Placement", description: "If the DropdownMenu conflicts with edge, it will auto-adjust it's placement", context: self) do + <<~RUBY + div(class: 'grid grid-cols-1 sm:grid-cols-3 gap-4') do + # -- TOP -- + DropdownMenu(options: { placement: 'top' }) do + DropdownMenuTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'top' } + end + DropdownMenuContent do + DropdownMenuLabel { "My Account" } + DropdownMenuSeparator + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + + DropdownMenu(options: { placement: 'top-start' }) do + DropdownMenuTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'top-start' } + end + DropdownMenuContent do + DropdownMenuLabel { "My Account" } + DropdownMenuSeparator + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + + DropdownMenu(options: { placement: 'top-end' }) do + DropdownMenuTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'top-end' } + end + DropdownMenuContent do + DropdownMenuLabel { "My Account" } + DropdownMenuSeparator + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + + # -- BOTTOM -- + DropdownMenu(options: { placement: 'bottom' }) do + DropdownMenuTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'bottom' } + end + DropdownMenuContent do + DropdownMenuLabel { "My Account" } + DropdownMenuSeparator + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + + DropdownMenu(options: { placement: 'bottom-start' }) do + DropdownMenuTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'bottom-start' } + end + DropdownMenuContent do + DropdownMenuLabel { "My Account" } + DropdownMenuSeparator + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + + DropdownMenu(options: { placement: 'bottom-end' }) do + DropdownMenuTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'bottom-end' } + end + DropdownMenuContent do + DropdownMenuLabel { "My Account" } + DropdownMenuSeparator + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + + # -- LEFT -- + DropdownMenu(options: { placement: 'left' }) do + DropdownMenuTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'left' } + end + DropdownMenuContent do + DropdownMenuLabel { "My Account" } + DropdownMenuSeparator + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + + DropdownMenu(options: { placement: 'left-start' }) do + DropdownMenuTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'left-start' } + end + DropdownMenuContent do + DropdownMenuLabel { "My Account" } + DropdownMenuSeparator + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + + DropdownMenu(options: { placement: 'left-end' }) do + DropdownMenuTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'left-end' } + end + DropdownMenuContent do + DropdownMenuLabel { "My Account" } + DropdownMenuSeparator + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + + # -- RIGHT -- + DropdownMenu(options: { placement: 'right' }) do + DropdownMenuTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'right' } + end + DropdownMenuContent do + DropdownMenuLabel { "My Account" } + DropdownMenuSeparator + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + + DropdownMenu(options: { placement: 'right-start' }) do + DropdownMenuTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'right-start' } + end + DropdownMenuContent do + DropdownMenuLabel { "My Account" } + DropdownMenuSeparator + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + + DropdownMenu(options: { placement: 'right-end' }) do + DropdownMenuTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'right-end' } + end + DropdownMenuContent do + DropdownMenuLabel { "My Account" } + DropdownMenuSeparator + DropdownMenuItem(href: '#') { "Profile" } + DropdownMenuItem(href: '#') { "Billing" } + DropdownMenuItem(href: '#') { "Team" } + DropdownMenuItem(href: '#') { "Subscription" } + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/dropdown_menu/dropdown_menu_item.rb b/gem/lib/ruby_ui/dropdown_menu/dropdown_menu_item.rb similarity index 100% rename from lib/ruby_ui/dropdown_menu/dropdown_menu_item.rb rename to gem/lib/ruby_ui/dropdown_menu/dropdown_menu_item.rb diff --git a/lib/ruby_ui/dropdown_menu/dropdown_menu_label.rb b/gem/lib/ruby_ui/dropdown_menu/dropdown_menu_label.rb similarity index 100% rename from lib/ruby_ui/dropdown_menu/dropdown_menu_label.rb rename to gem/lib/ruby_ui/dropdown_menu/dropdown_menu_label.rb diff --git a/lib/ruby_ui/dropdown_menu/dropdown_menu_separator.rb b/gem/lib/ruby_ui/dropdown_menu/dropdown_menu_separator.rb similarity index 100% rename from lib/ruby_ui/dropdown_menu/dropdown_menu_separator.rb rename to gem/lib/ruby_ui/dropdown_menu/dropdown_menu_separator.rb diff --git a/lib/ruby_ui/dropdown_menu/dropdown_menu_trigger.rb b/gem/lib/ruby_ui/dropdown_menu/dropdown_menu_trigger.rb similarity index 100% rename from lib/ruby_ui/dropdown_menu/dropdown_menu_trigger.rb rename to gem/lib/ruby_ui/dropdown_menu/dropdown_menu_trigger.rb diff --git a/lib/ruby_ui/form/form.rb b/gem/lib/ruby_ui/form/form.rb similarity index 100% rename from lib/ruby_ui/form/form.rb rename to gem/lib/ruby_ui/form/form.rb diff --git a/gem/lib/ruby_ui/form/form_docs.rb b/gem/lib/ruby_ui/form/form_docs.rb new file mode 100644 index 00000000..499ad4b6 --- /dev/null +++ b/gem/lib/ruby_ui/form/form_docs.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +class Views::Docs::Form < Views::Base + def view_template + component = "Form" + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Form", description: "Building forms with built-in client-side validations.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + Form(class: "w-2/3 space-y-6") do + FormField do + FormFieldLabel { "Default error" } + Input(placeholder: "Joel Drapper", required: true, minlength: "3") { "Joel Drapper" } + FormFieldHint() + FormFieldError() + end + Button(type: "submit") { "Save" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Disabled", context: self) do + <<~RUBY + FormField do + FormFieldLabel { "Disabled" } + Input(disabled: true, placeholder: "Joel Drapper", required: true, minlength: "3") { "Joel Drapper" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Aria Disabled", context: self) do + <<~RUBY + FormField do + FormFieldLabel { "Aria Disabled" } + Input(aria: {disabled: "true"}, placeholder: "Joel Drapper", required: true, minlength: "3") { "Joel Drapper" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Custom error message", context: self) do + <<~RUBY + Form(class: "w-2/3 space-y-6") do + FormField do + FormFieldLabel { "Custom error message" } + Input(placeholder: "joel@drapper.me", required: true, data_value_missing: "Custom error message") + FormFieldError() + end + Button(type: "submit") { "Save" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Backend error", context: self) do + <<~RUBY + Form(class: "w-2/3 space-y-6") do + FormField do + FormFieldLabel { "Backend error" } + Input(placeholder: "Joel Drapper", required: true) + FormFieldError { "Error from backend" } + end + Button(type: "submit") { "Save" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Checkbox", context: self) do + <<~RUBY + Form(class: "w-2/3 space-y-6") do + FormField do + Checkbox(required: true) + label( + class: + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" + ) { " Accept terms and conditions " } + FormFieldError() + end + Button(type: "submit") { "Save" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Select", context: self) do + <<~RUBY + Form(class: "w-2/3 space-y-6") do + FormField do + FormFieldLabel { "Select" } + Select do + SelectInput(required: true) + SelectTrigger do + SelectValue(placeholder: "Select a fruit") + end + SelectContent() do + SelectGroup do + SelectLabel { "Fruits" } + SelectItem(value: "apple") { "Apple" } + SelectItem(value: "orange") { "Orange" } + SelectItem(value: "banana") { "Banana" } + SelectItem(value: "watermelon") { "Watermelon" } + end + end + end + FormFieldError() + end + Button(type: "submit") { "Save" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Combobox", context: self) do + <<~RUBY + Form(class: "w-2/3 space-y-6") do + FormField do + FormFieldLabel { "Combobox" } + + Combobox do + ComboboxTrigger placeholder: "Pick value" + + ComboboxPopover do + ComboboxSearchInput(placeholder: "Pick value or type anything") + + ComboboxList do + ComboboxEmptyState { "No result" } + + ComboboxListGroup label: "Fruits" do + ComboboxItem do + ComboboxRadio(name: "food", value: "apple", required: true) + span { "Apple" } + end + + ComboboxItem do + ComboboxRadio(name: "food", value: "banana", required: true) + span { "Banana" } + end + end + + ComboboxListGroup label: "Vegetable" do + ComboboxItem do + ComboboxRadio(name: "food", value: "brocoli", required: true) + span { "Broccoli" } + end + + ComboboxItem do + ComboboxRadio(name: "food", value: "carrot", required: true) + span { "Carrot" } + end + end + + ComboboxListGroup label: "Others" do + ComboboxItem do + ComboboxRadio(name: "food", value: "chocolate", required: true) + span { "Chocolate" } + end + + ComboboxItem do + ComboboxRadio(name: "food", value: "milk", required: true) + span { "Milk" } + end + end + end + end + end + + FormFieldError() + end + Button(type: "submit") { "Save" } + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/form/form_field.rb b/gem/lib/ruby_ui/form/form_field.rb similarity index 100% rename from lib/ruby_ui/form/form_field.rb rename to gem/lib/ruby_ui/form/form_field.rb diff --git a/gem/lib/ruby_ui/form/form_field_controller.js b/gem/lib/ruby_ui/form/form_field_controller.js new file mode 100644 index 00000000..b7af9394 --- /dev/null +++ b/gem/lib/ruby_ui/form/form_field_controller.js @@ -0,0 +1,61 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = ["input", "error"]; + static values = { shouldValidate: false }; + + connect() { + if (this.hasErrorTarget) { + if (this.errorTarget.textContent) { + this.shouldValidateValue = true; + } else { + this.errorTarget.classList.add("hidden"); + } + } + } + + onInvalid(error) { + error.preventDefault(); + + this.shouldValidateValue = true; + this.#setErrorMessage(); + } + + onInput() { + this.#setErrorMessage(); + } + + onChange() { + this.#setErrorMessage(); + } + + #setErrorMessage() { + if (!this.shouldValidateValue) return; + + if (this.inputTarget.validity.valid) { + this.errorTarget.textContent = ""; + this.errorTarget.classList.add("hidden"); + } else { + this.errorTarget.textContent = this.#getValidationMessage(); + this.errorTarget.classList.remove("hidden"); + } + } + + #getValidationMessage() { + let errorMessage; + + const { validity, dataset, validationMessage } = this.inputTarget; + + if (validity.tooLong) errorMessage = dataset.tooLong; + if (validity.tooShort) errorMessage = dataset.tooShort; + if (validity.badInput) errorMessage = dataset.badInput; + if (validity.typeMismatch) errorMessage = dataset.typeMismatch; + if (validity.stepMismatch) errorMessage = dataset.stepMismatch; + if (validity.valueMissing) errorMessage = dataset.valueMissing; + if (validity.rangeOverflow) errorMessage = dataset.rangeOverflow; + if (validity.rangeUnderflow) errorMessage = dataset.rangeUnderflow; + if (validity.patternMismatch) errorMessage = dataset.patternMismatch; + + return errorMessage || validationMessage; + } +} diff --git a/lib/ruby_ui/form/form_field_error.rb b/gem/lib/ruby_ui/form/form_field_error.rb similarity index 100% rename from lib/ruby_ui/form/form_field_error.rb rename to gem/lib/ruby_ui/form/form_field_error.rb diff --git a/lib/ruby_ui/form/form_field_hint.rb b/gem/lib/ruby_ui/form/form_field_hint.rb similarity index 100% rename from lib/ruby_ui/form/form_field_hint.rb rename to gem/lib/ruby_ui/form/form_field_hint.rb diff --git a/lib/ruby_ui/form/form_field_label.rb b/gem/lib/ruby_ui/form/form_field_label.rb similarity index 100% rename from lib/ruby_ui/form/form_field_label.rb rename to gem/lib/ruby_ui/form/form_field_label.rb diff --git a/lib/ruby_ui/hover_card/hover_card.rb b/gem/lib/ruby_ui/hover_card/hover_card.rb similarity index 100% rename from lib/ruby_ui/hover_card/hover_card.rb rename to gem/lib/ruby_ui/hover_card/hover_card.rb diff --git a/lib/ruby_ui/hover_card/hover_card_content.rb b/gem/lib/ruby_ui/hover_card/hover_card_content.rb similarity index 100% rename from lib/ruby_ui/hover_card/hover_card_content.rb rename to gem/lib/ruby_ui/hover_card/hover_card_content.rb diff --git a/gem/lib/ruby_ui/hover_card/hover_card_controller.js b/gem/lib/ruby_ui/hover_card/hover_card_controller.js new file mode 100644 index 00000000..23510570 --- /dev/null +++ b/gem/lib/ruby_ui/hover_card/hover_card_controller.js @@ -0,0 +1,144 @@ +import { Controller } from "@hotwired/stimulus"; +import tippy from "tippy.js"; + +export default class extends Controller { + static targets = ["trigger", "content", "menuItem"]; + static values = { + options: { + type: Object, + default: {}, + }, + // make content width of the trigger element (true/false) + matchWidth: { + type: Boolean, + default: false, + } + } + + connect() { + this.boundHandleKeydown = this.handleKeydown.bind(this); // Bind the function so we can remove it later + this.initializeTippy(); + this.selectedIndex = -1; + } + + disconnect() { + this.destroyTippy(); + } + + initializeTippy() { + const defaultOptions = { + content: this.contentTarget.innerHTML, + allowHTML: true, + interactive: true, + onShow: (instance) => { + this.matchWidthValue && this.setContentWidth(instance); // ensure content width matches trigger width + this.addEventListeners(); + }, + onHide: () => { + this.removeEventListeners(); + this.deselectAll(); + }, + popperOptions: { + modifiers: [ + { + name: "offset", + options: { + offset: [0, 4] + }, + }, + ], + } + }; + + const mergedOptions = { ...this.optionsValue, ...defaultOptions }; + this.tippy = tippy(this.triggerTarget, mergedOptions); + } + + destroyTippy() { + if (this.tippy) { + this.tippy.destroy(); + } + } + + setContentWidth(instance) { + // box-sizing: border-box + const content = instance.popper.querySelector('.tippy-content'); + if (content) { + content.style.width = `${instance.reference.offsetWidth}px`; + } + } + + handleContextMenu(event) { + event.preventDefault(); + this.open(); + } + + open() { + this.tippy.show(); + } + + close() { + this.tippy.hide(); + } + + handleKeydown(e) { + // return if no menu items (one line fix for) + if (this.menuItemTargets.length === 0) { return; } + + if (e.key === 'ArrowDown') { + e.preventDefault(); + this.updateSelectedItem(1); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + this.updateSelectedItem(-1); + } else if (e.key === 'Enter' && this.selectedIndex !== -1) { + e.preventDefault(); + this.menuItemTargets[this.selectedIndex].click(); + } + } + + updateSelectedItem(direction) { + // Check if any of the menuItemTargets have aria-selected="true" and set the selectedIndex to that index + this.menuItemTargets.forEach((item, index) => { + if (item.getAttribute('aria-selected') === 'true') { + this.selectedIndex = index; + } + }); + + if (this.selectedIndex >= 0) { + this.toggleAriaSelected(this.menuItemTargets[this.selectedIndex], false); + } + + this.selectedIndex += direction; + + if (this.selectedIndex < 0) { + this.selectedIndex = this.menuItemTargets.length - 1; + } else if (this.selectedIndex >= this.menuItemTargets.length) { + this.selectedIndex = 0; + } + + this.toggleAriaSelected(this.menuItemTargets[this.selectedIndex], true); + } + + toggleAriaSelected(element, isSelected) { + // Add or remove attribute + if (isSelected) { + element.setAttribute('aria-selected', 'true'); + } else { + element.removeAttribute('aria-selected'); + } + } + + deselectAll() { + this.menuItemTargets.forEach(item => this.toggleAriaSelected(item, false)); + this.selectedIndex = -1; + } + + addEventListeners() { + document.addEventListener('keydown', this.boundHandleKeydown); + } + + removeEventListeners() { + document.removeEventListener('keydown', this.boundHandleKeydown); + } +} diff --git a/lib/ruby_ui/hover_card/hover_card_docs.rb b/gem/lib/ruby_ui/hover_card/hover_card_docs.rb similarity index 100% rename from lib/ruby_ui/hover_card/hover_card_docs.rb rename to gem/lib/ruby_ui/hover_card/hover_card_docs.rb diff --git a/lib/ruby_ui/hover_card/hover_card_trigger.rb b/gem/lib/ruby_ui/hover_card/hover_card_trigger.rb similarity index 100% rename from lib/ruby_ui/hover_card/hover_card_trigger.rb rename to gem/lib/ruby_ui/hover_card/hover_card_trigger.rb diff --git a/lib/ruby_ui/input/input.rb b/gem/lib/ruby_ui/input/input.rb similarity index 100% rename from lib/ruby_ui/input/input.rb rename to gem/lib/ruby_ui/input/input.rb diff --git a/gem/lib/ruby_ui/input/input_docs.rb b/gem/lib/ruby_ui/input/input_docs.rb new file mode 100644 index 00000000..460247bc --- /dev/null +++ b/gem/lib/ruby_ui/input/input_docs.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +class Views::Docs::Input < Views::Base + def view_template + component = "Input" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Input", description: "Displays a form input field or a component that looks like an input field.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Email", context: self) do + <<~RUBY + div(class: 'grid w-full max-w-sm items-center gap-1.5') do + Input(type: "email", placeholder: "Email") + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "File", context: self) do + <<~RUBY + div(class: "grid w-full max-w-sm items-center gap-1.5") do + label(for: "picture") { "Picture" } + Input(type: "file", id: "picture") + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Disabled", context: self) do + <<~RUBY + div(class: 'grid w-full max-w-sm items-center gap-1.5') do + Input(disabled: true, type: "email", placeholder: "Email") + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Aria Disabled", context: self) do + <<~RUBY + div(class: 'grid w-full max-w-sm items-center gap-1.5') do + Input(aria: {disabled: "true"}, type: "email", placeholder: "Email") + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "With label", context: self) do + <<~RUBY + div(class: 'grid w-full max-w-sm items-center gap-1.5') do + label(for: "email1") { "Email" } + Input(type: "email", placeholder: "Email", id: "email1") + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "With button", context: self) do + <<~RUBY + div(class: 'flex w-full max-w-sm items-center space-x-2') do + Input(type: "email", placeholder: "Email") + Button { "Subscribe" } + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/link/link.rb b/gem/lib/ruby_ui/link/link.rb similarity index 100% rename from lib/ruby_ui/link/link.rb rename to gem/lib/ruby_ui/link/link.rb diff --git a/gem/lib/ruby_ui/link/link_docs.rb b/gem/lib/ruby_ui/link/link_docs.rb new file mode 100644 index 00000000..3a17a3a8 --- /dev/null +++ b/gem/lib/ruby_ui/link/link_docs.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +class Views::Docs::Link < Views::Base + def view_template + component = "Link" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Link", description: "Displays a link that looks like a button or underline link.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", description: "This is the default appearance of a Link", context: self) do + <<~RUBY + Link(href: "#") { "Link" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Aria Disabled", context: self) do + <<~RUBY + Link(aria: {disabled: "true"}, href: "#") { "Link" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Primary", description: "This is the primary variant of a Link", context: self) do + <<~RUBY + Link(href: "#", variant: :primary) { "Primary" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Secondary", description: "This is the secondary variant of a Link", context: self) do + <<~RUBY + Link(href: "#", variant: :secondary) { "Secondary" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Destructive", description: "This is the destructive variant of a Link", context: self) do + <<~RUBY + Link(href: "#", variant: :destructive) { "Destructive" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Icon", description: "This is the icon variant of a Link", context: self) do + <<~RUBY + Link(href: "#", variant: :outline, icon: true) do + chevron_icon + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "With Icon", description: "This is the primary variant of a Link with an icon", context: self) do + <<~RUBY + Link(href: "#", variant: :primary) do + email_icon + span { "Login with Email" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Ghost", description: "This is the ghost variant of a Link", context: self) do + <<~RUBY + Link(href: "#", variant: :ghost) { "Ghost" } + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end + + private + + def chevron_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 20 20", + fill: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z", + clip_rule: "evenodd" + ) + end + end + + def email_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" + ) + end + end +end diff --git a/lib/ruby_ui/masked_input/masked_input.rb b/gem/lib/ruby_ui/masked_input/masked_input.rb similarity index 100% rename from lib/ruby_ui/masked_input/masked_input.rb rename to gem/lib/ruby_ui/masked_input/masked_input.rb diff --git a/lib/ruby_ui/masked_input/masked_input_controller.js b/gem/lib/ruby_ui/masked_input/masked_input_controller.js similarity index 100% rename from lib/ruby_ui/masked_input/masked_input_controller.js rename to gem/lib/ruby_ui/masked_input/masked_input_controller.js diff --git a/gem/lib/ruby_ui/masked_input/masked_input_docs.rb b/gem/lib/ruby_ui/masked_input/masked_input_docs.rb new file mode 100644 index 00000000..25ea4986 --- /dev/null +++ b/gem/lib/ruby_ui/masked_input/masked_input_docs.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class Views::Docs::MaskedInput < Views::Base + def view_template + component = "MaskedInput" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "MaskedInput", description: "Displays a form input field with applied mask.") + + Heading(level: 2) { "Usage" } + + Text do + plain "For advanced usage, check out the " + InlineLink(href: "https://beholdr.github.io/maska/v3", target: "_blank") { "Maska website" } + plain "." + end + + render Docs::VisualCodeExample.new(title: "Phone number", context: self) do + <<~RUBY + div(class: 'grid w-full max-w-sm items-center gap-1.5') do + MaskedInput(data: {maska: "(##) #####-####"}) + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Hex color code", context: self) do + <<~RUBY + div(class: 'grid w-full max-w-sm items-center gap-1.5') do + MaskedInput(data: {maska: "!#HHHHHH", maska_tokens: "H:[0-9a-fA-F]"}) + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "CPF / CNPJ", context: self) do + <<~RUBY + div(class: 'grid w-full max-w-sm items-center gap-1.5') do + MaskedInput(data: {maska: "['###.###.###-##', '##.###.###/####-##']"}) + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/native_select/native_select.rb b/gem/lib/ruby_ui/native_select/native_select.rb similarity index 100% rename from lib/ruby_ui/native_select/native_select.rb rename to gem/lib/ruby_ui/native_select/native_select.rb diff --git a/gem/lib/ruby_ui/native_select/native_select_docs.rb b/gem/lib/ruby_ui/native_select/native_select_docs.rb new file mode 100644 index 00000000..98a6a76e --- /dev/null +++ b/gem/lib/ruby_ui/native_select/native_select_docs.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +class Views::Docs::NativeSelect < Views::Base + def view_template + component = "NativeSelect" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Native Select", description: "A styled native HTML select element with consistent design system integration.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Default", context: self) do + <<~RUBY + div(class: "grid w-full max-w-sm items-center gap-1.5") do + NativeSelect do + NativeSelectOption(value: "") { "Select a fruit" } + NativeSelectOption(value: "apple") { "Apple" } + NativeSelectOption(value: "banana") { "Banana" } + NativeSelectOption(value: "blueberry") { "Blueberry" } + NativeSelectOption(value: "pineapple") { "Pineapple" } + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Groups", description: "Use NativeSelectGroup to organize options into categories.", context: self) do + <<~RUBY + div(class: "grid w-full max-w-sm items-center gap-1.5") do + NativeSelect do + NativeSelectOption(value: "") { "Select a department" } + NativeSelectGroup(label: "Engineering") do + NativeSelectOption(value: "frontend") { "Frontend" } + NativeSelectOption(value: "backend") { "Backend" } + NativeSelectOption(value: "devops") { "DevOps" } + end + NativeSelectGroup(label: "Sales") do + NativeSelectOption(value: "account_executive") { "Account Executive" } + NativeSelectOption(value: "sales_development") { "Sales Development" } + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Disabled", description: "Add the disabled attribute to the NativeSelect component to disable the select.", context: self) do + <<~RUBY + div(class: "grid w-full max-w-sm items-center gap-1.5") do + NativeSelect(disabled: true) do + NativeSelectOption(value: "") { "Select a fruit" } + NativeSelectOption(value: "apple") { "Apple" } + NativeSelectOption(value: "banana") { "Banana" } + NativeSelectOption(value: "blueberry") { "Blueberry" } + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Invalid", description: "Use aria-invalid to show validation errors.", context: self) do + <<~RUBY + div(class: "grid w-full max-w-sm items-center gap-1.5") do + NativeSelect(aria: {invalid: "true"}) do + NativeSelectOption(value: "") { "Select a fruit" } + NativeSelectOption(value: "apple") { "Apple" } + NativeSelectOption(value: "banana") { "Banana" } + NativeSelectOption(value: "blueberry") { "Blueberry" } + end + end + RUBY + end + + Heading(level: 2) { "Native Select vs Select" } + + div(class: "space-y-2 text-sm text-muted-foreground") do + p { "NativeSelect: Choose for native browser behavior, superior performance, or mobile-optimized dropdowns." } + p { "Select: Choose for custom styling, animations, or complex interactions." } + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/native_select/native_select_group.rb b/gem/lib/ruby_ui/native_select/native_select_group.rb similarity index 100% rename from lib/ruby_ui/native_select/native_select_group.rb rename to gem/lib/ruby_ui/native_select/native_select_group.rb diff --git a/lib/ruby_ui/native_select/native_select_icon.rb b/gem/lib/ruby_ui/native_select/native_select_icon.rb similarity index 100% rename from lib/ruby_ui/native_select/native_select_icon.rb rename to gem/lib/ruby_ui/native_select/native_select_icon.rb diff --git a/lib/ruby_ui/native_select/native_select_option.rb b/gem/lib/ruby_ui/native_select/native_select_option.rb similarity index 100% rename from lib/ruby_ui/native_select/native_select_option.rb rename to gem/lib/ruby_ui/native_select/native_select_option.rb diff --git a/lib/ruby_ui/pagination/pagination.rb b/gem/lib/ruby_ui/pagination/pagination.rb similarity index 100% rename from lib/ruby_ui/pagination/pagination.rb rename to gem/lib/ruby_ui/pagination/pagination.rb diff --git a/lib/ruby_ui/pagination/pagination_content.rb b/gem/lib/ruby_ui/pagination/pagination_content.rb similarity index 100% rename from lib/ruby_ui/pagination/pagination_content.rb rename to gem/lib/ruby_ui/pagination/pagination_content.rb diff --git a/gem/lib/ruby_ui/pagination/pagination_docs.rb b/gem/lib/ruby_ui/pagination/pagination_docs.rb new file mode 100644 index 00000000..b7987383 --- /dev/null +++ b/gem/lib/ruby_ui/pagination/pagination_docs.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +class Views::Docs::Pagination < Views::Base + def view_template + component = "Pagination" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Pagination", description: "Pagination with page navigation, next and previous links.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", description: "This is the default appearance of a Pagination", context: self) do + <<~RUBY + Pagination do + PaginationContent do + PaginationItem(href: "#") do + chevrons_left_icon + plain "First" + end + PaginationItem(href: "#") do + chevron_left_icon + plain "Prev" + end + + PaginationEllipsis + + PaginationItem(href: "#") { "4" } + PaginationItem(href: "#", active: true) { "5" } + PaginationItem(href: "#") { "6" } + + PaginationEllipsis + + PaginationItem(href: "#") do + plain "Next" + chevron_right_icon + end + PaginationItem(href: "#") do + plain "Last" + chevrons_right_icon + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end + + private + + def chevrons_left_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + fill: "none", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "h-4 w-4" + ) do |s| + s.path(stroke: "none", d: "M0 0h24v24H0z", fill: "none") + s.path(d: "M11 7l-5 5l5 5") + s.path(d: "M17 7l-5 5l5 5") + end + end + + def chevron_left_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + fill: "none", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "h-4 w-4" + ) do |s| + s.path(stroke: "none", d: "M0 0h24v24H0z", fill: "none") + s.path(d: "M15 6l-6 6l6 6") + end + end + + def chevrons_right_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + fill: "none", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "h-4 w-4" + ) do |s| + s.path(stroke: "none", d: "M0 0h24v24H0z", fill: "none") + s.path(d: "M7 7l5 5l-5 5") + s.path(d: "M13 7l5 5l-5 5") + end + end + + def chevron_right_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + fill: "none", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "h-4 w-4" + ) do |s| + s.path(stroke: "none", d: "M0 0h24v24H0z", fill: "none") + s.path(d: "M9 6l6 6l-6 6") + end + end +end diff --git a/lib/ruby_ui/pagination/pagination_ellipsis.rb b/gem/lib/ruby_ui/pagination/pagination_ellipsis.rb similarity index 100% rename from lib/ruby_ui/pagination/pagination_ellipsis.rb rename to gem/lib/ruby_ui/pagination/pagination_ellipsis.rb diff --git a/lib/ruby_ui/pagination/pagination_item.rb b/gem/lib/ruby_ui/pagination/pagination_item.rb similarity index 100% rename from lib/ruby_ui/pagination/pagination_item.rb rename to gem/lib/ruby_ui/pagination/pagination_item.rb diff --git a/lib/ruby_ui/popover/popover.rb b/gem/lib/ruby_ui/popover/popover.rb similarity index 100% rename from lib/ruby_ui/popover/popover.rb rename to gem/lib/ruby_ui/popover/popover.rb diff --git a/lib/ruby_ui/popover/popover_content.rb b/gem/lib/ruby_ui/popover/popover_content.rb similarity index 100% rename from lib/ruby_ui/popover/popover_content.rb rename to gem/lib/ruby_ui/popover/popover_content.rb diff --git a/gem/lib/ruby_ui/popover/popover_controller.js b/gem/lib/ruby_ui/popover/popover_controller.js new file mode 100644 index 00000000..9b847764 --- /dev/null +++ b/gem/lib/ruby_ui/popover/popover_controller.js @@ -0,0 +1,107 @@ +import { Controller } from "@hotwired/stimulus"; +import { + computePosition, + flip, + shift, + offset, + autoUpdate, +} from "@floating-ui/dom"; + +export default class extends Controller { + static targets = ["trigger", "content"]; + static values = { + open: { type: Boolean, default: false }, + options: { type: Object, default: {} }, + trigger: { type: String, default: "hover" }, + }; + + connect() { + this.closeTimeout = null; + this.cleanup = null; + this.addEventListeners(); + } + + disconnect() { + this.removeEventListeners(); + if (this.cleanup) { + this.cleanup(); + } + } + + addEventListeners() { + if (this.triggerValue === "hover") { + this.triggerTarget.addEventListener("mouseenter", this.handleMouseEnter); + this.triggerTarget.addEventListener("mouseleave", this.handleMouseLeave); + this.contentTarget.addEventListener("mouseenter", this.handleMouseEnter); + this.contentTarget.addEventListener("mouseleave", this.handleMouseLeave); + } else if (this.triggerValue === "click") { + this.triggerTarget.addEventListener("click", this.handleClick); + document.addEventListener("click", this.handleOutsideClick); + } + } + + removeEventListeners() { + this.triggerTarget.removeEventListener("mouseenter", this.handleMouseEnter); + this.triggerTarget.removeEventListener("mouseleave", this.handleMouseLeave); + this.contentTarget.removeEventListener("mouseenter", this.handleMouseEnter); + this.contentTarget.removeEventListener("mouseleave", this.handleMouseLeave); + this.triggerTarget.removeEventListener("click", this.handleClick); + document.removeEventListener("click", this.handleOutsideClick); + } + + handleMouseEnter = () => { + clearTimeout(this.closeTimeout); + this.openValue = true; + this.showPopover(); + }; + + handleMouseLeave = () => { + this.closeTimeout = setTimeout(() => { + this.openValue = false; + this.hidePopover(); + }, 100); + }; + + handleClick = (event) => { + event.stopPropagation(); + this.openValue = !this.openValue; + this.openValue ? this.showPopover() : this.hidePopover(); + }; + + handleOutsideClick = (event) => { + if (!this.element.contains(event.target) && this.openValue) { + this.openValue = false; + this.hidePopover(); + } + }; + + showPopover() { + this.contentTarget.classList.remove("hidden"); + this.updatePosition(); + } + + hidePopover() { + this.contentTarget.classList.add("hidden"); + if (this.cleanup) { + this.cleanup(); + } + } + + updatePosition() { + if (this.cleanup) { + this.cleanup(); + } + + this.cleanup = autoUpdate(this.triggerTarget, this.contentTarget, () => { + computePosition(this.triggerTarget, this.contentTarget, { + placement: this.optionsValue.placement || "bottom", + middleware: [flip(), shift(), offset(8)], + }).then(({ x, y }) => { + Object.assign(this.contentTarget.style, { + left: `${x}px`, + top: `${y}px`, + }); + }); + }); + } +} diff --git a/gem/lib/ruby_ui/popover/popover_docs.rb b/gem/lib/ruby_ui/popover/popover_docs.rb new file mode 100644 index 00000000..1046dc25 --- /dev/null +++ b/gem/lib/ruby_ui/popover/popover_docs.rb @@ -0,0 +1,971 @@ +# frozen_string_literal: true + +class Views::Docs::Popover < Views::Base + def view_template + component = "Popover" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Popover", description: "Displays rich content in a portal, triggered by a button.") + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + Popover do + PopoverTrigger(class: 'w-full') do + Button(variant: :outline) { "Open Popover" } + end + PopoverContent(class: 'w-40') do + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Profile" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Settings" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" + ) + end + plain "Logout" + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Placement", context: self) do + <<~RUBY + div(class: 'grid grid-cols-1 sm:grid-cols-3 gap-4') do + # -- TOP -- + Popover(options: { placement: 'top' }) do + PopoverTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'top' } + end + PopoverContent(class: 'w-40') do + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Profile" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Settings" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" + ) + end + plain "Logout" + end + end + end + + Popover(options: { placement: 'top-start' }) do + PopoverTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'top-start' } + end + PopoverContent(class: 'w-40') do + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Profile" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Settings" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" + ) + end + plain "Logout" + end + end + end + + Popover(options: { placement: 'top-end' }) do + PopoverTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'top-end' } + end + PopoverContent(class: 'w-40') do + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Profile" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Settings" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" + ) + end + plain "Logout" + end + end + end + + # -- RIGHT -- + Popover(options: { placement: 'right' }) do + PopoverTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'right' } + end + PopoverContent(class: 'w-40') do + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Profile" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Settings" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" + ) + end + plain "Logout" + end + end + end + + Popover(options: { placement: 'right-start' }) do + PopoverTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'right-start' } + end + PopoverContent(class: 'w-40') do + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Profile" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Settings" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" + ) + end + plain "Logout" + end + end + end + + Popover(options: { placement: 'right-end' }) do + PopoverTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'right-end' } + end + PopoverContent(class: 'w-40') do + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Profile" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Settings" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" + ) + end + plain "Logout" + end + end + end + + # -- LEFT -- + Popover(options: { placement: 'left' }) do + PopoverTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'left' } + end + PopoverContent(class: 'w-40') do + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Profile" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Settings" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" + ) + end + plain "Logout" + end + end + end + + Popover(options: { placement: 'left-start' }) do + PopoverTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'left-start' } + end + PopoverContent(class: 'w-40') do + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Profile" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Settings" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" + ) + end + plain "Logout" + end + end + end + + Popover(options: { placement: 'left-end' }) do + PopoverTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'left-end' } + end + PopoverContent(class: 'w-40') do + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Profile" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Settings" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" + ) + end + plain "Logout" + end + end + end + + # -- BOTTOM -- + Popover(options: { placement: 'bottom' }) do + PopoverTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'bottom' } + end + PopoverContent(class: 'w-40') do + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Profile" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Settings" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" + ) + end + plain "Logout" + end + end + end + + Popover(options: { placement: 'bottom-start' }) do + PopoverTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'bottom-start' } + end + PopoverContent(class: 'w-40') do + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Profile" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Settings" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" + ) + end + plain "Logout" + end + end + end + + Popover(options: { placement: 'bottom-end' }) do + PopoverTrigger(class: 'w-full') do + Button(variant: :outline, class: 'w-full justify-center') { 'bottom-end' } + end + PopoverContent(class: 'w-40') do + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Profile" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Settings" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" + ) + end + plain "Logout" + end + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Trigger", context: self) do + <<~RUBY + Popover(options: { trigger: 'click' }) do + PopoverTrigger(class: 'w-full') do + Button(variant: :outline) { "Click" } + end + PopoverContent(class: 'w-40') do + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Profile" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + ) + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" + ) + end + plain "Settings" + end + Link(href: "#", variant: :ghost, class: 'w-full justify-start pl-2') do + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4 mr-2" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" + ) + end + plain "Logout" + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/popover/popover_trigger.rb b/gem/lib/ruby_ui/popover/popover_trigger.rb similarity index 100% rename from lib/ruby_ui/popover/popover_trigger.rb rename to gem/lib/ruby_ui/popover/popover_trigger.rb diff --git a/lib/ruby_ui/progress/progress.rb b/gem/lib/ruby_ui/progress/progress.rb similarity index 100% rename from lib/ruby_ui/progress/progress.rb rename to gem/lib/ruby_ui/progress/progress.rb diff --git a/gem/lib/ruby_ui/progress/progress_docs.rb b/gem/lib/ruby_ui/progress/progress_docs.rb new file mode 100644 index 00000000..dc2a7c47 --- /dev/null +++ b/gem/lib/ruby_ui/progress/progress_docs.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class Views::Docs::Progress < Views::Base + def view_template + component = "Progress" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Progress", description: "Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.") + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + Progress(value: 50, class: "w-[60%]") + RUBY + end + + render Docs::VisualCodeExample.new(title: "With custom indicator color", context: self) do + <<~RUBY + Progress(value: 35, class: "w-[60%] [&>*]:bg-success") + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/radio_button/radio_button.rb b/gem/lib/ruby_ui/radio_button/radio_button.rb similarity index 100% rename from lib/ruby_ui/radio_button/radio_button.rb rename to gem/lib/ruby_ui/radio_button/radio_button.rb diff --git a/gem/lib/ruby_ui/radio_button/radio_button_docs.rb b/gem/lib/ruby_ui/radio_button/radio_button_docs.rb new file mode 100644 index 00000000..15d0e024 --- /dev/null +++ b/gem/lib/ruby_ui/radio_button/radio_button_docs.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class Views::Docs::RadioButton < Views::Base + def view_template + component = "RadioButton" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Radio Button", description: "A control that allows users to make a single selection from a list of options.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + div(class: "flex items-center space-x-2") do + RadioButton(id: "default") + FormFieldLabel(for: "default") { "Default" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Checked", context: self) do + <<~RUBY + div(class: "flex items-center space-x-2") do + RadioButton(id: "checked", checked: true) + FormFieldLabel(for: "checked") { "Checked" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Disabled", context: self) do + <<~RUBY + div(class: "flex flex-row items-center gap-2") do + RadioButton(class: "peer",id: "disabled", disabled: true) + FormFieldLabel(for: "disabled") { "Disabled" } + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Aria Disabled", context: self) do + <<~RUBY + div(class: "flex flex-row items-center gap-2") do + RadioButton(class: "peer", id: "aria-disabled", aria: {disabled: "true"}) + FormFieldLabel(for: "aria-disabled") { "Aria Disabled" } + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/select/select.rb b/gem/lib/ruby_ui/select/select.rb similarity index 100% rename from lib/ruby_ui/select/select.rb rename to gem/lib/ruby_ui/select/select.rb diff --git a/lib/ruby_ui/select/select_content.rb b/gem/lib/ruby_ui/select/select_content.rb similarity index 100% rename from lib/ruby_ui/select/select_content.rb rename to gem/lib/ruby_ui/select/select_content.rb diff --git a/gem/lib/ruby_ui/select/select_controller.js b/gem/lib/ruby_ui/select/select_controller.js new file mode 100644 index 00000000..918b3067 --- /dev/null +++ b/gem/lib/ruby_ui/select/select_controller.js @@ -0,0 +1,171 @@ +import { Controller } from "@hotwired/stimulus"; +import { computePosition, autoUpdate, offset, flip } from "@floating-ui/dom"; + +export default class extends Controller { + static targets = ["trigger", "content", "input", "value", "item"]; + static values = { open: Boolean }; + static outlets = ["ruby-ui--select-item"]; + + constructor(...args) { + super(...args); + this.cleanup; + } + + connect() { + this.setFloatingElement(); + this.generateItemsIds(); + } + + disconnect() { + this.cleanup(); + } + + selectItem(event) { + event.preventDefault(); + + this.rubyUiSelectItemOutlets.forEach((item) => + item.handleSelectItem(event), + ); + + const oldValue = this.inputTarget.value; + const newValue = event.target.dataset.value; + + this.inputTarget.value = newValue; + this.valueTarget.innerText = event.target.innerText; + + this.dispatchOnChange(oldValue, newValue); + this.closeContent(); + } + + onClick() { + this.toogleContent(); + + if (this.openValue) { + this.setFocusAndCurrent(); + } else { + this.resetCurrent(); + } + } + + handleKeyDown(event) { + event.preventDefault(); + + const currentIndex = this.itemTargets.findIndex( + (item) => item.getAttribute("aria-current") === "true", + ); + + if (currentIndex + 1 < this.itemTargets.length) { + this.itemTargets[currentIndex].removeAttribute("aria-current"); + this.setAriaCurrentAndActiveDescendant(currentIndex + 1); + } + } + + handleKeyUp(event) { + event.preventDefault(); + + const currentIndex = this.itemTargets.findIndex( + (item) => item.getAttribute("aria-current") === "true", + ); + + if (currentIndex > 0) { + this.itemTargets[currentIndex].removeAttribute("aria-current"); + this.setAriaCurrentAndActiveDescendant(currentIndex - 1); + } + } + + handleEsc(event) { + event.preventDefault(); + this.closeContent(); + } + + setFocusAndCurrent() { + const selectedItem = this.itemTargets.find( + (item) => item.getAttribute("aria-selected") === "true", + ); + + if (selectedItem) { + selectedItem.focus({ preventScroll: true }); + selectedItem.setAttribute("aria-current", "true"); + this.triggerTarget.setAttribute( + "aria-activedescendant", + selectedItem.getAttribute("id"), + ); + } else { + this.itemTarget.focus({ preventScroll: true }); + this.itemTarget.setAttribute("aria-current", "true"); + this.triggerTarget.setAttribute( + "aria-activedescendant", + this.itemTarget.getAttribute("id"), + ); + } + } + + resetCurrent() { + this.itemTargets.forEach((item) => item.removeAttribute("aria-current")); + } + + clickOutside(event) { + if (!this.openValue) return; + if (this.element.contains(event.target)) return; + + event.preventDefault(); + this.toogleContent(); + } + + toogleContent() { + this.openValue = !this.openValue; + this.contentTarget.classList.toggle("hidden"); + this.triggerTarget.setAttribute("aria-expanded", this.openValue); + } + + setFloatingElement() { + this.cleanup = autoUpdate(this.triggerTarget, this.contentTarget, () => { + computePosition(this.triggerTarget, this.contentTarget, { + middleware: [offset(4), flip()], + }).then(({ x, y }) => { + Object.assign(this.contentTarget.style, { + left: `${x}px`, + top: `${y}px`, + }); + }); + }); + } + + generateItemsIds() { + const contentId = this.contentTarget.getAttribute("id"); + this.triggerTarget.setAttribute("aria-controls", contentId); + + this.itemTargets.forEach((item, index) => { + item.id = `${contentId}-${index}`; + }); + } + + setAriaCurrentAndActiveDescendant(currentIndex) { + const currentItem = this.itemTargets[currentIndex]; + currentItem.focus({ preventScroll: true }); + currentItem.setAttribute("aria-current", "true"); + this.triggerTarget.setAttribute( + "aria-activedescendant", + currentItem.getAttribute("id"), + ); + } + + closeContent() { + this.toogleContent(); + this.resetCurrent(); + + this.triggerTarget.setAttribute("aria-activedescendant", true); + this.triggerTarget.focus({ preventScroll: true }); + } + + dispatchOnChange(oldValue, newValue) { + if (oldValue === newValue) return; + + const event = new InputEvent("change", { + bubbles: true, + cancelable: true, + }); + + this.inputTarget.dispatchEvent(event); + } +} diff --git a/gem/lib/ruby_ui/select/select_docs.rb b/gem/lib/ruby_ui/select/select_docs.rb new file mode 100644 index 00000000..0209b22f --- /dev/null +++ b/gem/lib/ruby_ui/select/select_docs.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +class Views::Docs::Select < Views::Base + def view_template + component = "Select" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Select", description: "Displays a list of options for the user to pick from—triggered by a button.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Select (Deconstructed)", context: self) do + <<~RUBY + Select(class: "w-56") do + SelectInput(value: "apple", id: "select-a-fruit") + + SelectTrigger do + SelectValue(placeholder: "Select a fruit", id: "select-a-fruit") { "Apple" } + end + + SelectContent(outlet_id: "select-a-fruit") do + SelectGroup do + SelectLabel { "Fruits" } + SelectItem(value: "apple") { "Apple" } + SelectItem(value: "orange") { "Orange" } + SelectItem(value: "banana") { "Banana" } + SelectItem(value: "watermelon") { "Watermelon" } + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Pre-selected Item", context: self) do + <<~RUBY + Select(class: "w-56") do + SelectInput(value: "banana", id: "select-preselected-fruit") + + SelectTrigger do + SelectValue(placeholder: "Select a fruit", id: "select-preselected-fruit") { "Banana" } + end + + SelectContent(outlet_id: "select-preselected-fruit") do + SelectGroup do + SelectLabel { "Fruits" } + SelectItem(value: "apple") { "Apple" } + SelectItem(value: "orange") { "Orange" } + SelectItem(value: "banana") { "Banana" } + SelectItem(value: "watermelon") { "Watermelon" } + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Disabled", context: self) do + <<~RUBY + Select(class: "w-56") do + SelectInput(value: "apple", id: "select-a-fruit") + + SelectTrigger(disabled: true) do + SelectValue(placeholder: "Select a fruit", id: "select-a-fruit") { "Apple" } + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Data Disabled", context: self) do + <<~RUBY + Select(class: "w-56") do + SelectInput(value: "apple", id: "select-a-fruit") + + SelectTrigger do + SelectValue(placeholder: "Select a fruit", id: "select-a-fruit") { "Apple" } + end + + SelectContent(outlet_id: "select-a-fruit") do + SelectGroup do + SelectLabel { "Fruits" } + SelectItem(data: {disabled: true}, value: "apple") { "Apple" } + SelectItem(value: "orange") { "Orange" } + SelectItem(value: "banana") { "Banana" } + SelectItem(data: {disabled: true}, value: "watermelon") { "Watermelon" } + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Aria Disabled Trigger", context: self) do + <<~RUBY + Select(class: "w-56") do + SelectInput(value: "apple", id: "select-a-fruit") + + SelectTrigger(aria: {disabled: "true"}) do + SelectValue(placeholder: "Select a fruit", id: "select-a-fruit") { "Apple" } + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Aria Disabled Item", context: self) do + <<~RUBY + Select(class: "w-56") do + SelectInput(value: "apple", id: "select-a-fruit") + + SelectTrigger do + SelectValue(placeholder: "Select a fruit", id: "select-a-fruit") { "Apple" } + end + + SelectContent(outlet_id: "select-a-fruit") do + SelectGroup do + SelectLabel { "Fruits" } + SelectItem(aria: {disabled: "true"}, value: "apple") { "Apple" } + SelectItem(value: "orange") { "Orange" } + SelectItem(value: "banana") { "Banana" } + SelectItem(aria: {disabled: "true"}, value: "watermelon") { "Watermelon" } + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/select/select_group.rb b/gem/lib/ruby_ui/select/select_group.rb similarity index 100% rename from lib/ruby_ui/select/select_group.rb rename to gem/lib/ruby_ui/select/select_group.rb diff --git a/lib/ruby_ui/select/select_input.rb b/gem/lib/ruby_ui/select/select_input.rb similarity index 100% rename from lib/ruby_ui/select/select_input.rb rename to gem/lib/ruby_ui/select/select_input.rb diff --git a/lib/ruby_ui/select/select_item.rb b/gem/lib/ruby_ui/select/select_item.rb similarity index 100% rename from lib/ruby_ui/select/select_item.rb rename to gem/lib/ruby_ui/select/select_item.rb diff --git a/gem/lib/ruby_ui/select/select_item_controller.js b/gem/lib/ruby_ui/select/select_item_controller.js new file mode 100644 index 00000000..3d76417d --- /dev/null +++ b/gem/lib/ruby_ui/select/select_item_controller.js @@ -0,0 +1,11 @@ +import { Controller } from "@hotwired/stimulus"; +export default class extends Controller { + + handleSelectItem({ target }) { + if (this.element.dataset.value == target.dataset.value) { + this.element.setAttribute("aria-selected", true); + } else { + this.element.removeAttribute("aria-selected"); + } + } +} diff --git a/lib/ruby_ui/select/select_label.rb b/gem/lib/ruby_ui/select/select_label.rb similarity index 100% rename from lib/ruby_ui/select/select_label.rb rename to gem/lib/ruby_ui/select/select_label.rb diff --git a/lib/ruby_ui/select/select_trigger.rb b/gem/lib/ruby_ui/select/select_trigger.rb similarity index 100% rename from lib/ruby_ui/select/select_trigger.rb rename to gem/lib/ruby_ui/select/select_trigger.rb diff --git a/lib/ruby_ui/select/select_value.rb b/gem/lib/ruby_ui/select/select_value.rb similarity index 100% rename from lib/ruby_ui/select/select_value.rb rename to gem/lib/ruby_ui/select/select_value.rb diff --git a/lib/ruby_ui/separator/separator.rb b/gem/lib/ruby_ui/separator/separator.rb similarity index 100% rename from lib/ruby_ui/separator/separator.rb rename to gem/lib/ruby_ui/separator/separator.rb diff --git a/gem/lib/ruby_ui/separator/separator_docs.rb b/gem/lib/ruby_ui/separator/separator_docs.rb new file mode 100644 index 00000000..c5d980d6 --- /dev/null +++ b/gem/lib/ruby_ui/separator/separator_docs.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class Views::Docs::Separator < Views::Base + def view_template + component = "Separator" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Separator", description: "Visually or semantically separates content.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + div do + div(class: "space-y-1") do + h4(class: "text-sm font-medium leading-none") { "RubyUI" } + p(class: "text-sm text-muted-foreground") { "An open-source UI component library." } + end + Separator(class: "my-4") + div(class: "flex h-5 items-center space-x-4 text-sm") do + div { "Blog" } + Separator(as: :hr, orientation: :vertical) + div { "Docs" } + Separator(orientation: :vertical) + div { "Source" } + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/sheet/sheet.rb b/gem/lib/ruby_ui/sheet/sheet.rb similarity index 100% rename from lib/ruby_ui/sheet/sheet.rb rename to gem/lib/ruby_ui/sheet/sheet.rb diff --git a/lib/ruby_ui/sheet/sheet_content.rb b/gem/lib/ruby_ui/sheet/sheet_content.rb similarity index 100% rename from lib/ruby_ui/sheet/sheet_content.rb rename to gem/lib/ruby_ui/sheet/sheet_content.rb diff --git a/gem/lib/ruby_ui/sheet/sheet_content_controller.js b/gem/lib/ruby_ui/sheet/sheet_content_controller.js new file mode 100644 index 00000000..8df0712b --- /dev/null +++ b/gem/lib/ruby_ui/sheet/sheet_content_controller.js @@ -0,0 +1,7 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + close() { + this.element.remove() + } +} diff --git a/gem/lib/ruby_ui/sheet/sheet_controller.js b/gem/lib/ruby_ui/sheet/sheet_controller.js new file mode 100644 index 00000000..45e98637 --- /dev/null +++ b/gem/lib/ruby_ui/sheet/sheet_controller.js @@ -0,0 +1,9 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["content"] + + open() { + document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML) + } +} diff --git a/lib/ruby_ui/sheet/sheet_description.rb b/gem/lib/ruby_ui/sheet/sheet_description.rb similarity index 100% rename from lib/ruby_ui/sheet/sheet_description.rb rename to gem/lib/ruby_ui/sheet/sheet_description.rb diff --git a/gem/lib/ruby_ui/sheet/sheet_docs.rb b/gem/lib/ruby_ui/sheet/sheet_docs.rb new file mode 100644 index 00000000..8865c0a2 --- /dev/null +++ b/gem/lib/ruby_ui/sheet/sheet_docs.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +class Views::Docs::Sheet < Views::Base + def view_template + component = "Sheet" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Sheet", description: "Extends the Sheet component to display content that complements the main content of the screen.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + Sheet do + SheetTrigger do + Button(variant: :outline) { "Open Sheet" } + end + SheetContent(class: 'sm:max-w-sm') do + SheetHeader do + SheetTitle { "Edit profile" } + SheetDescription { "Make changes to your profile here. Click save when you're done." } + end + + SheetMiddle do + label { "Name" } + Input(placeholder: "Joel Drapper") { "Joel Drapper" } + label { "Email" } + Input(placeholder: "joel@drapper.me") + end + SheetFooter do + Button(variant: :outline, data: { action: 'click->ruby-ui--sheet-content#close' }) { "Cancel" } + Button(type: "submit") { "Save" } + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Side", description: "Use the side property to indicate the edge of the screen where the component will appear.", context: self) do + <<~RUBY + div(class: 'grid grid-cols-2 gap-4') do + # -- TOP -- + Sheet do + SheetTrigger do + Button(variant: :outline, class: 'w-full justify-center') { :top } + end + SheetContent(side: :top, class: ("sm:max-w-sm" if [:left, :right].include?(:top))) do + SheetHeader do + SheetTitle { "Edit profile" } + SheetDescription { "Make changes to your profile here. Click save when you're done." } + end + Form do + SheetMiddle do + label { "Name" } + Input(placeholder: "Joel Drapper") { "Joel Drapper" } + + label { "Email" } + Input(placeholder: "joel@drapper.me") + end + SheetFooter do + Button(variant: :outline, data: { action: 'click->ruby-ui--sheet-content#close' }) { "Cancel" } + Button(type: "submit") { "Save" } + end + end + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/sheet/sheet_footer.rb b/gem/lib/ruby_ui/sheet/sheet_footer.rb similarity index 100% rename from lib/ruby_ui/sheet/sheet_footer.rb rename to gem/lib/ruby_ui/sheet/sheet_footer.rb diff --git a/lib/ruby_ui/sheet/sheet_header.rb b/gem/lib/ruby_ui/sheet/sheet_header.rb similarity index 100% rename from lib/ruby_ui/sheet/sheet_header.rb rename to gem/lib/ruby_ui/sheet/sheet_header.rb diff --git a/lib/ruby_ui/sheet/sheet_middle.rb b/gem/lib/ruby_ui/sheet/sheet_middle.rb similarity index 100% rename from lib/ruby_ui/sheet/sheet_middle.rb rename to gem/lib/ruby_ui/sheet/sheet_middle.rb diff --git a/lib/ruby_ui/sheet/sheet_title.rb b/gem/lib/ruby_ui/sheet/sheet_title.rb similarity index 100% rename from lib/ruby_ui/sheet/sheet_title.rb rename to gem/lib/ruby_ui/sheet/sheet_title.rb diff --git a/lib/ruby_ui/sheet/sheet_trigger.rb b/gem/lib/ruby_ui/sheet/sheet_trigger.rb similarity index 100% rename from lib/ruby_ui/sheet/sheet_trigger.rb rename to gem/lib/ruby_ui/sheet/sheet_trigger.rb diff --git a/lib/ruby_ui/shortcut_key/shortcut_key.rb b/gem/lib/ruby_ui/shortcut_key/shortcut_key.rb similarity index 100% rename from lib/ruby_ui/shortcut_key/shortcut_key.rb rename to gem/lib/ruby_ui/shortcut_key/shortcut_key.rb diff --git a/gem/lib/ruby_ui/shortcut_key/shortcut_key_docs.rb b/gem/lib/ruby_ui/shortcut_key/shortcut_key_docs.rb new file mode 100644 index 00000000..ea1ee43e --- /dev/null +++ b/gem/lib/ruby_ui/shortcut_key/shortcut_key_docs.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Views::Docs::ShortcutKey < Views::Base + def view_template + component = "ShortcutKey" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Shortcut Key", description: "A component for displaying keyboard shortcuts.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + div(class: "flex flex-col items-center gap-y-4") do + ShortcutKey do + span(class: "text-xs") { "⌘" } + plain "K" + end + p(class: "text-muted-foreground text-sm text-center") { "Note this does not trigger anything, it is purely a visual prompt" } + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/sidebar/collapsible_sidebar.rb b/gem/lib/ruby_ui/sidebar/collapsible_sidebar.rb similarity index 100% rename from lib/ruby_ui/sidebar/collapsible_sidebar.rb rename to gem/lib/ruby_ui/sidebar/collapsible_sidebar.rb diff --git a/lib/ruby_ui/sidebar/mobile_sidebar.rb b/gem/lib/ruby_ui/sidebar/mobile_sidebar.rb similarity index 100% rename from lib/ruby_ui/sidebar/mobile_sidebar.rb rename to gem/lib/ruby_ui/sidebar/mobile_sidebar.rb diff --git a/lib/ruby_ui/sidebar/non_collapsible_sidebar.rb b/gem/lib/ruby_ui/sidebar/non_collapsible_sidebar.rb similarity index 100% rename from lib/ruby_ui/sidebar/non_collapsible_sidebar.rb rename to gem/lib/ruby_ui/sidebar/non_collapsible_sidebar.rb diff --git a/lib/ruby_ui/sidebar/sidebar.rb b/gem/lib/ruby_ui/sidebar/sidebar.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar.rb rename to gem/lib/ruby_ui/sidebar/sidebar.rb diff --git a/lib/ruby_ui/sidebar/sidebar_content.rb b/gem/lib/ruby_ui/sidebar/sidebar_content.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_content.rb rename to gem/lib/ruby_ui/sidebar/sidebar_content.rb diff --git a/gem/lib/ruby_ui/sidebar/sidebar_controller.js b/gem/lib/ruby_ui/sidebar/sidebar_controller.js new file mode 100644 index 00000000..c789438d --- /dev/null +++ b/gem/lib/ruby_ui/sidebar/sidebar_controller.js @@ -0,0 +1,67 @@ +import { Controller } from "@hotwired/stimulus"; + +const SIDEBAR_COOKIE_NAME = "sidebar_state"; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const State = { + EXPANDED: "expanded", + COLLAPSED: "collapsed", +}; +const MOBILE_BREAKPOINT = 768; + +export default class extends Controller { + static targets = ["sidebar", "mobileSidebar"]; + + sidebarTargetConnected() { + const { state, collapsibleKind } = this.sidebarTarget.dataset; + + this.open = state === State.EXPANDED; + this.collapsibleKind = collapsibleKind; + } + + toggle(e) { + e.preventDefault(); + + if (this.#isMobile()) { + this.#openMobileSidebar(); + + return; + } + + this.open = !this.open; + this.onToggle(); + } + + onToggle() { + this.#updateSidebarState(); + this.#persistSidebarState(); + } + + #updateSidebarState() { + if (!this.hasSidebarTarget) { + return; + } + + const { dataset } = this.sidebarTarget; + + dataset.state = this.open ? State.EXPANDED : State.COLLAPSED; + dataset.collapsible = this.open ? "" : this.collapsibleKind; + } + + #persistSidebarState() { + document.cookie = `${SIDEBAR_COOKIE_NAME}=${this.open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + } + + #isMobile() { + return window.innerWidth < MOBILE_BREAKPOINT; + } + + #openMobileSidebar() { + if (!this.hasMobileSidebarTarget) { + return; + } + + this.mobileSidebarTarget.dispatchEvent( + new CustomEvent("ruby--ui-sidebar:open"), + ); + } +} diff --git a/gem/lib/ruby_ui/sidebar/sidebar_docs.rb b/gem/lib/ruby_ui/sidebar/sidebar_docs.rb new file mode 100644 index 00000000..165872fa --- /dev/null +++ b/gem/lib/ruby_ui/sidebar/sidebar_docs.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +class Views::Docs::Sidebar < Views::Base + def view_template + component = "Sidebar" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Sidebar", description: "A composable, themeable and customizable sidebar component.") + + Heading(level: 2) { "Usage" } + + Alert do + info_icon + AlertTitle { "Requirements" } + AlertDescription { "The sidebar component depends on the following components:" } + ul(class: "list-disc list-inside") do + li do + InlineLink(href: docs_sheet_path, target: "_blank", class: "inline-flex items-center gap-2") do + span { "Sheet" } + external_icon_link + end + end + li do + div(class: "inline-flex items-center gap-2") do + InlineLink(href: docs_separator_path, target: "_blank") { "Separator" } + external_icon_link + end + end + end + end + + render Docs::VisualCodeExample.new(title: "Example", src: "/docs/sidebar/example", context: self) do + Views::Docs::Sidebar::Example::CODE + end + + render Docs::VisualCodeExample.new(title: "Inset variant", src: "/docs/sidebar/inset", context: self) do + Views::Docs::Sidebar::InsetExample::CODE + end + + render Docs::VisualCodeExample.new(title: "Dialog variant", context: self) do + <<~RUBY + Dialog(data: {action: "ruby-ui--dialog:connect->ruby-ui--dialog#open"}) do + DialogTrigger do + Button { "Open Dialog" } + end + DialogContent(class: "grid overflow-hidden p-0 md:max-h-[500px] md:max-w-[700px] lg:max-w-[800px]") do + SidebarWrapper(class: "items-start") do + Sidebar(collapsible: :none, class: "hidden md:flex") do + SidebarContent do + SidebarGroup do + SidebarGroupContent do + SidebarMenu do + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + search_icon() + span { "Search" } + end + end + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#", active: true) do + home_icon() + span { "Home" } + end + end + SidebarMenuItem do + SidebarMenuButton(as: :a, href: "#") do + inbox_icon() + span { "Inbox" } + end + end + end + end + end + end + end + main(class: "flex h-[480px] flex-1 flex-col overflow-hidden") do + end + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end + + def search_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-search" + ) do |s| + s.circle(cx: "11", cy: "11", r: "8") + s.path(d: "M21 21L16.7 16.7") + end + end + + def home_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-house" + ) do |s| + s.path(d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8") + s.path(d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z") + end + end + + def inbox_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + width: "24", + height: "24", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-inbox" + ) do |s| + s.polyline(points: "22 12 16 12 14 15 10 15 8 12 2 12") + s.path(d: "M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z") + end + end + + def external_icon_link + svg( + xmlns: "http://www.w3.org/2000/svg", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + stroke_width: "2", + stroke_linecap: "round", + stroke_linejoin: "round", + class: "lucide lucide-external-link-icon lucide-external-link size-3" + ) do |s| + s.path(d: "M15 3h6v6") + s.path(d: "M10 14 21 3") + s.path(d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6") + end + end + + def info_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-5 h-5" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 01.67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 11-.671-1.34l.041-.022zM12 9a.75.75 0 100-1.5.75.75 0 000 1.5z", + clip_rule: "evenodd" + ) + end + end +end diff --git a/lib/ruby_ui/sidebar/sidebar_footer.rb b/gem/lib/ruby_ui/sidebar/sidebar_footer.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_footer.rb rename to gem/lib/ruby_ui/sidebar/sidebar_footer.rb diff --git a/lib/ruby_ui/sidebar/sidebar_group.rb b/gem/lib/ruby_ui/sidebar/sidebar_group.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_group.rb rename to gem/lib/ruby_ui/sidebar/sidebar_group.rb diff --git a/lib/ruby_ui/sidebar/sidebar_group_action.rb b/gem/lib/ruby_ui/sidebar/sidebar_group_action.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_group_action.rb rename to gem/lib/ruby_ui/sidebar/sidebar_group_action.rb diff --git a/lib/ruby_ui/sidebar/sidebar_group_content.rb b/gem/lib/ruby_ui/sidebar/sidebar_group_content.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_group_content.rb rename to gem/lib/ruby_ui/sidebar/sidebar_group_content.rb diff --git a/lib/ruby_ui/sidebar/sidebar_group_label.rb b/gem/lib/ruby_ui/sidebar/sidebar_group_label.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_group_label.rb rename to gem/lib/ruby_ui/sidebar/sidebar_group_label.rb diff --git a/lib/ruby_ui/sidebar/sidebar_header.rb b/gem/lib/ruby_ui/sidebar/sidebar_header.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_header.rb rename to gem/lib/ruby_ui/sidebar/sidebar_header.rb diff --git a/lib/ruby_ui/sidebar/sidebar_input.rb b/gem/lib/ruby_ui/sidebar/sidebar_input.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_input.rb rename to gem/lib/ruby_ui/sidebar/sidebar_input.rb diff --git a/lib/ruby_ui/sidebar/sidebar_inset.rb b/gem/lib/ruby_ui/sidebar/sidebar_inset.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_inset.rb rename to gem/lib/ruby_ui/sidebar/sidebar_inset.rb diff --git a/lib/ruby_ui/sidebar/sidebar_menu.rb b/gem/lib/ruby_ui/sidebar/sidebar_menu.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_menu.rb rename to gem/lib/ruby_ui/sidebar/sidebar_menu.rb diff --git a/lib/ruby_ui/sidebar/sidebar_menu_action.rb b/gem/lib/ruby_ui/sidebar/sidebar_menu_action.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_menu_action.rb rename to gem/lib/ruby_ui/sidebar/sidebar_menu_action.rb diff --git a/lib/ruby_ui/sidebar/sidebar_menu_badge.rb b/gem/lib/ruby_ui/sidebar/sidebar_menu_badge.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_menu_badge.rb rename to gem/lib/ruby_ui/sidebar/sidebar_menu_badge.rb diff --git a/lib/ruby_ui/sidebar/sidebar_menu_button.rb b/gem/lib/ruby_ui/sidebar/sidebar_menu_button.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_menu_button.rb rename to gem/lib/ruby_ui/sidebar/sidebar_menu_button.rb diff --git a/lib/ruby_ui/sidebar/sidebar_menu_item.rb b/gem/lib/ruby_ui/sidebar/sidebar_menu_item.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_menu_item.rb rename to gem/lib/ruby_ui/sidebar/sidebar_menu_item.rb diff --git a/lib/ruby_ui/sidebar/sidebar_menu_skeleton.rb b/gem/lib/ruby_ui/sidebar/sidebar_menu_skeleton.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_menu_skeleton.rb rename to gem/lib/ruby_ui/sidebar/sidebar_menu_skeleton.rb diff --git a/lib/ruby_ui/sidebar/sidebar_menu_sub.rb b/gem/lib/ruby_ui/sidebar/sidebar_menu_sub.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_menu_sub.rb rename to gem/lib/ruby_ui/sidebar/sidebar_menu_sub.rb diff --git a/lib/ruby_ui/sidebar/sidebar_menu_sub_button.rb b/gem/lib/ruby_ui/sidebar/sidebar_menu_sub_button.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_menu_sub_button.rb rename to gem/lib/ruby_ui/sidebar/sidebar_menu_sub_button.rb diff --git a/lib/ruby_ui/sidebar/sidebar_menu_sub_item.rb b/gem/lib/ruby_ui/sidebar/sidebar_menu_sub_item.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_menu_sub_item.rb rename to gem/lib/ruby_ui/sidebar/sidebar_menu_sub_item.rb diff --git a/lib/ruby_ui/sidebar/sidebar_rail.rb b/gem/lib/ruby_ui/sidebar/sidebar_rail.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_rail.rb rename to gem/lib/ruby_ui/sidebar/sidebar_rail.rb diff --git a/lib/ruby_ui/sidebar/sidebar_separator.rb b/gem/lib/ruby_ui/sidebar/sidebar_separator.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_separator.rb rename to gem/lib/ruby_ui/sidebar/sidebar_separator.rb diff --git a/lib/ruby_ui/sidebar/sidebar_trigger.rb b/gem/lib/ruby_ui/sidebar/sidebar_trigger.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_trigger.rb rename to gem/lib/ruby_ui/sidebar/sidebar_trigger.rb diff --git a/lib/ruby_ui/sidebar/sidebar_wrapper.rb b/gem/lib/ruby_ui/sidebar/sidebar_wrapper.rb similarity index 100% rename from lib/ruby_ui/sidebar/sidebar_wrapper.rb rename to gem/lib/ruby_ui/sidebar/sidebar_wrapper.rb diff --git a/lib/ruby_ui/skeleton/skeleton.rb b/gem/lib/ruby_ui/skeleton/skeleton.rb similarity index 100% rename from lib/ruby_ui/skeleton/skeleton.rb rename to gem/lib/ruby_ui/skeleton/skeleton.rb diff --git a/gem/lib/ruby_ui/skeleton/skeleton_docs.rb b/gem/lib/ruby_ui/skeleton/skeleton_docs.rb new file mode 100644 index 00000000..44d311f9 --- /dev/null +++ b/gem/lib/ruby_ui/skeleton/skeleton_docs.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Views::Docs::Skeleton < Views::Base + def view_template + component = "Skeleton" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Skeleton", description: "Use to show a placeholder while content is loading.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + div(class: "flex items-center space-x-4") do + Skeleton(class: "h-12 w-12 rounded-full") + div(class: "space-y-2") do + Skeleton(class: "h-4 w-[250px]") + Skeleton(class: "h-4 w-[200px]") + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/switch/switch.rb b/gem/lib/ruby_ui/switch/switch.rb similarity index 100% rename from lib/ruby_ui/switch/switch.rb rename to gem/lib/ruby_ui/switch/switch.rb diff --git a/gem/lib/ruby_ui/switch/switch_docs.rb b/gem/lib/ruby_ui/switch/switch_docs.rb new file mode 100644 index 00000000..8f4dcdc7 --- /dev/null +++ b/gem/lib/ruby_ui/switch/switch_docs.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class Views::Docs::Switch < Views::Base + def view_template + component = "Switch" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Switch", description: "A control that allows the user to toggle between checked and not checked.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Default", context: self) do + <<~RUBY + Switch(name: "switch") + RUBY + end + + render Docs::VisualCodeExample.new(title: "Checked", context: self) do + <<~RUBY + Switch(name: "switch", checked: true) + RUBY + end + + render Docs::VisualCodeExample.new(title: "Disabled", context: self) do + <<~RUBY + Switch(name: "switch", disabled: true) + RUBY + end + + render Docs::VisualCodeExample.new(title: "Aria Disabled", context: self) do + <<~RUBY + Switch(name: "switch", aria: {disabled: "true"}) + RUBY + end + + render Docs::VisualCodeExample.new(title: "With flag include_hidden false", context: self) do + <<~RUBY + # Supports the creation of a hidden input to be used in forms inspired by the Ruby on Rails implementation of check_box. Default is true. + Switch(name: "switch", include_hidden: false) + RUBY + end + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/table/table.rb b/gem/lib/ruby_ui/table/table.rb similarity index 100% rename from lib/ruby_ui/table/table.rb rename to gem/lib/ruby_ui/table/table.rb diff --git a/lib/ruby_ui/table/table_body.rb b/gem/lib/ruby_ui/table/table_body.rb similarity index 100% rename from lib/ruby_ui/table/table_body.rb rename to gem/lib/ruby_ui/table/table_body.rb diff --git a/lib/ruby_ui/table/table_caption.rb b/gem/lib/ruby_ui/table/table_caption.rb similarity index 100% rename from lib/ruby_ui/table/table_caption.rb rename to gem/lib/ruby_ui/table/table_caption.rb diff --git a/lib/ruby_ui/table/table_cell.rb b/gem/lib/ruby_ui/table/table_cell.rb similarity index 100% rename from lib/ruby_ui/table/table_cell.rb rename to gem/lib/ruby_ui/table/table_cell.rb diff --git a/gem/lib/ruby_ui/table/table_docs.rb b/gem/lib/ruby_ui/table/table_docs.rb new file mode 100644 index 00000000..8ac4d8c0 --- /dev/null +++ b/gem/lib/ruby_ui/table/table_docs.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +class Views::Docs::Table < Views::Base + Invoice = Struct.new(:identifier, :status, :method, :amount, keyword_init: true) + User = Struct.new(:avatar_url, :name, :username, :commits, :github_url, keyword_init: true) + + def view_template + component = "Table" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-8") do + render Docs::Header.new(title: "Table", description: "A responsive table component.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Without builder", context: self) do + <<~RUBY + Table do + TableCaption { "Employees at Acme inc." } + TableHeader do + TableRow do + TableHead { "Name" } + TableHead { "Email" } + TableHead { "Status" } + TableHead(class: "text-right") { "Role" } + end + end + TableBody do + invoices.each do |invoice| + TableRow do + TableCell(class: 'font-medium') { invoice.identifier } + TableCell { render_status_badge(invoice.status) } + TableCell { invoice.method } + TableCell(class: "text-right") { format_amount(invoice.amount) } + end + end + end + TableFooter do + TableRow do + TableHead(class: "font-medium", colspan: 3) { "Total" } + TableHead(class: "font-medium text-right") { format_amount(invoices.sum(&:amount)) } + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end + + private + + def invoices + [ + Invoice.new(identifier: "INV-0001", status: "Active", method: "Credit Card", amount: 100), + Invoice.new(identifier: "INV-0002", status: "Active", method: "Bank Transfer", amount: 230), + Invoice.new(identifier: "INV-0003", status: "Pending", method: "PayPal", amount: 350), + Invoice.new(identifier: "INV-0004", status: "Inactive", method: "Credit Card", amount: 100) + ] + end + + def users + [ + User.new(avatar_url: "https://avatars.githubusercontent.com/u/246692?v=4", name: "Joel Drapper", username: "joeldrapper", commits: 404), + User.new(avatar_url: "https://avatars.githubusercontent.com/u/33979976?v=4", name: "Alexandre Ruban", username: "alexandreruban", commits: 16), + User.new(avatar_url: "https://avatars.githubusercontent.com/u/77887?v=4", name: "Will Cosgrove", username: "willcosgrove", commits: 12), + User.new(avatar_url: "https://avatars.githubusercontent.com/u/3025661?v=4", name: "Stephann V.", username: "stephannv", commits: 8), + User.new(avatar_url: "https://avatars.githubusercontent.com/u/6411752?v=4", name: "Marco Roth", username: "marcoroth", commits: 8) + ] + end + + def github_link(user) + "https://github.com/#{user.username}" + end + + def github_icon + svg(viewbox: "0 0 438.549 438.549", class: "h-4 w-4") do |s| + s.path( + fill: "currentColor", + d: + "M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z" + ) + end + end + + def format_amount(amount) + "$#{amount}.00" + end + + def render_status_badge(status) + case status.downcase + when "active" + Badge(variant: :success, size: :sm) { status } + when "inactive" + Badge(variant: :destructive, size: :sm) { status } + when "pending" + Badge(variant: :warning, size: :sm) { status } + end + end +end diff --git a/lib/ruby_ui/table/table_footer.rb b/gem/lib/ruby_ui/table/table_footer.rb similarity index 100% rename from lib/ruby_ui/table/table_footer.rb rename to gem/lib/ruby_ui/table/table_footer.rb diff --git a/lib/ruby_ui/table/table_head.rb b/gem/lib/ruby_ui/table/table_head.rb similarity index 100% rename from lib/ruby_ui/table/table_head.rb rename to gem/lib/ruby_ui/table/table_head.rb diff --git a/lib/ruby_ui/table/table_header.rb b/gem/lib/ruby_ui/table/table_header.rb similarity index 100% rename from lib/ruby_ui/table/table_header.rb rename to gem/lib/ruby_ui/table/table_header.rb diff --git a/lib/ruby_ui/table/table_row.rb b/gem/lib/ruby_ui/table/table_row.rb similarity index 100% rename from lib/ruby_ui/table/table_row.rb rename to gem/lib/ruby_ui/table/table_row.rb diff --git a/lib/ruby_ui/tabs/tabs.rb b/gem/lib/ruby_ui/tabs/tabs.rb similarity index 100% rename from lib/ruby_ui/tabs/tabs.rb rename to gem/lib/ruby_ui/tabs/tabs.rb diff --git a/lib/ruby_ui/tabs/tabs_content.rb b/gem/lib/ruby_ui/tabs/tabs_content.rb similarity index 100% rename from lib/ruby_ui/tabs/tabs_content.rb rename to gem/lib/ruby_ui/tabs/tabs_content.rb diff --git a/gem/lib/ruby_ui/tabs/tabs_controller.js b/gem/lib/ruby_ui/tabs/tabs_controller.js new file mode 100644 index 00000000..e46d69c4 --- /dev/null +++ b/gem/lib/ruby_ui/tabs/tabs_controller.js @@ -0,0 +1,45 @@ +import { Controller } from "@hotwired/stimulus"; + +// Connects to data-controller="ruby-ui--tabs" +export default class extends Controller { + static targets = ["trigger", "content"]; + static values = { active: String }; + + connect() { + if (!this.hasActiveValue && this.triggerTargets.length > 0) { + this.activeValue = this.triggerTargets[0].dataset.value; + } + } + + show(e) { + this.activeValue = e.currentTarget.dataset.value; + } + + activeValueChanged(currentValue, previousValue) { + if (currentValue == "" || currentValue == previousValue) return; + + this.contentTargets.forEach((el) => { + el.classList.add("hidden"); + }); + + this.triggerTargets.forEach((el) => { + el.dataset.state = "inactive"; + }); + + this.activeContentTarget() && + this.activeContentTarget().classList.remove("hidden"); + this.activeTriggerTarget().dataset.state = "active"; + } + + activeTriggerTarget() { + return this.triggerTargets.find( + (el) => el.dataset.value == this.activeValue, + ); + } + + activeContentTarget() { + return this.contentTargets.find( + (el) => el.dataset.value == this.activeValue, + ); + } +} diff --git a/gem/lib/ruby_ui/tabs/tabs_docs.rb b/gem/lib/ruby_ui/tabs/tabs_docs.rb new file mode 100644 index 00000000..33b6ff34 --- /dev/null +++ b/gem/lib/ruby_ui/tabs/tabs_docs.rb @@ -0,0 +1,211 @@ +# frozen_string_literal: true + +class Views::Docs::Tabs < Views::Base + Repo = Struct.new(:github_url, :name, :stars, :version, keyword_init: true) + + def view_template + component = "Tabs" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Tabs", description: "A set of layered sections of content—known as tab panels—that are displayed one at a time.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + Tabs(default_value: "account", class: 'w-96') do + TabsList do + TabsTrigger(value: "account") { "Account" } + TabsTrigger(value: "password") { "Password" } + end + TabsContent(value: "account") do + div(class: "rounded-lg border p-6 space-y-4 bg-background text-foreground") do + div(class: "space-y-0") do + Text(size: "4", weight: "semibold") { "Account" } + Text(size: "2", class: "text-muted-foreground") { "Update your account details." } + end + end + end + TabsContent(value: "password") do + div(class: "rounded-lg border p-6 space-y-4 bg-background text-foreground") do + div do + Text(size: "4", weight: "semibold") { "Password" } + Text(size: "2", class: "text-muted-foreground") { "Change your password here. After saving, you'll be logged out." } + end + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Disabled", context: self) do + <<~RUBY + Tabs(default_value: "account", class: 'w-96') do + TabsList do + TabsTrigger(disabled: true, value: "account") { "Account" } + TabsTrigger(disabled: true, value: "password") { "Password" } + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Aria Disabled", context: self) do + <<~RUBY + Tabs(default_value: "account", class: 'w-96') do + TabsList do + TabsTrigger(aria: {disabled: "true"}, value: "account") { "Account" } + TabsTrigger(aria: {disabled: "true"}, value: "password") { "Password" } + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Full width", context: self) do + <<~RUBY + Tabs(default_value: "overview", class: 'w-96') do + TabsList(class: 'w-full grid grid-cols-2') do + TabsTrigger(value: "overview") do + book_icon + span(class: 'ml-2') { "Overview" } + end + TabsTrigger(value: "repositories") do + repo_icon + span(class: 'ml-2') { "Repositories" } + end + end + TabsContent(value: "overview") do + div(class: "rounded-lg border p-6 bg-background text-foreground flex justify-between space-x-4") do + Avatar do + AvatarImage(src: "https://avatars.githubusercontent.com/u/246692?v=4", alt: "joeldrapper") + AvatarFallback { "JD" } + end + div(class: "space-y-4") do + div do + Text(size: "4", weight: "semibold") { "Joel Drapper" } + Text(size: "2", class: "text-muted-foreground") { "Creator of Phlex Components. Ruby on Rails developer." } + end + Link(href: "https://github.com/joeldrapper", variant: :outline, size: :sm) do + github_icon + span(class: 'ml-2') { "View profile" } + end + end + end + end + TabsContent(value: "repositories") do + div(class: "rounded-lg border p-6 space-y-4 bg-background text-foreground") do + repo = repositories.first + Link(href: repo.github_url, variant: :link, class: "pl-0") { repo.name } + Badge { repo.version } + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Change default value", context: self) do + <<~RUBY + Tabs(default: "password", class: 'w-96') do + TabsList do + TabsTrigger(value: "account") { "Account" } + TabsTrigger(value: "password") { "Password" } + end + TabsContent(value: "account") do + div(class: "rounded-lg border p-6 space-y-4 bg-background text-foreground") do + div(class: "space-y-0") do + Text(size: "4", weight: "semibold") { "Account" } + Text(size: "2", class: "text-muted-foreground") { "Update your account details." } + end + end + end + TabsContent(value: "password") do + div(class: "rounded-lg border p-6 space-y-4 bg-background text-foreground") do + div do + Text(size: "4", weight: "semibold") { "Password" } + Text(size: "2", class: "text-muted-foreground") { "Change your password here. After saving, you'll be logged out." } + end + end + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end + + private + + def book_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "2", + stroke: "currentColor", + class: "w-4 h-4 text-muted-foreground" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" + ) + end + end + + def repo_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "2", + stroke: "currentColor", + class: "w-4 h-4 text-muted-foreground" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" + ) + end + end + + def github_icon + svg(viewbox: "0 0 438.549 438.549", class: "h-4 w-4") do |s| + s.path( + fill: "currentColor", + d: + "M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 01-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z" + ) + end + end + + def star_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "2", + stroke: "currentColor", + class: "w-4 h-4 text-amber-500" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" + ) + end + end + + def repositories + [ + Repo.new(github_url: "https://github.com/phlex-ruby/phlex", name: "phlex", stars: 961, version: "v1.8.1"), + Repo.new(github_url: "https://github.com/joeldrapper/green_dots", name: "green_dots", stars: 40, version: "1.0"), + Repo.new(github_url: "https://github.com/joeldrapper/literal", name: "literal", stars: 96, version: "v0.1.0") + ] + end +end diff --git a/lib/ruby_ui/tabs/tabs_list.rb b/gem/lib/ruby_ui/tabs/tabs_list.rb similarity index 100% rename from lib/ruby_ui/tabs/tabs_list.rb rename to gem/lib/ruby_ui/tabs/tabs_list.rb diff --git a/lib/ruby_ui/tabs/tabs_trigger.rb b/gem/lib/ruby_ui/tabs/tabs_trigger.rb similarity index 100% rename from lib/ruby_ui/tabs/tabs_trigger.rb rename to gem/lib/ruby_ui/tabs/tabs_trigger.rb diff --git a/lib/ruby_ui/textarea/textarea.rb b/gem/lib/ruby_ui/textarea/textarea.rb similarity index 100% rename from lib/ruby_ui/textarea/textarea.rb rename to gem/lib/ruby_ui/textarea/textarea.rb diff --git a/gem/lib/ruby_ui/textarea/textarea_docs.rb b/gem/lib/ruby_ui/textarea/textarea_docs.rb new file mode 100644 index 00000000..ce83349a --- /dev/null +++ b/gem/lib/ruby_ui/textarea/textarea_docs.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class Views::Docs::Textarea < Views::Base + def view_template + component = "Textarea" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Textarea", description: "Displays a textarea field.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Textarea", context: self) do + <<~RUBY + div(class: "grid w-full max-w-sm items-center gap-1.5") do + Textarea(placeholder: "Textarea") + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Disabled", context: self) do + <<~RUBY + div(class: "grid w-full max-w-sm items-center gap-1.5") do + Textarea(disabled: true, placeholder: "Disabled") + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "Aria Disabled", context: self) do + <<~RUBY + div(class: "grid w-full max-w-sm items-center gap-1.5") do + Textarea(aria: {disabled: "true"}, placeholder: "Aria Disabled") + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "With FormField", context: self) do + <<~RUBY + div(class: "grid w-full max-w-sm items-center gap-1.5") do + FormField do + FormFieldLabel(for: "textarea") { "Textarea" } + FormFieldHint { "This is a textarea" } + Textarea(placeholder: "Textarea", id: "textarea") + FormFieldError() + end + end + RUBY + end + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end +end diff --git a/lib/ruby_ui/theme_toggle/set_dark_mode.rb b/gem/lib/ruby_ui/theme_toggle/set_dark_mode.rb similarity index 100% rename from lib/ruby_ui/theme_toggle/set_dark_mode.rb rename to gem/lib/ruby_ui/theme_toggle/set_dark_mode.rb diff --git a/lib/ruby_ui/theme_toggle/set_light_mode.rb b/gem/lib/ruby_ui/theme_toggle/set_light_mode.rb similarity index 100% rename from lib/ruby_ui/theme_toggle/set_light_mode.rb rename to gem/lib/ruby_ui/theme_toggle/set_light_mode.rb diff --git a/lib/ruby_ui/theme_toggle/theme_toggle.rb b/gem/lib/ruby_ui/theme_toggle/theme_toggle.rb similarity index 100% rename from lib/ruby_ui/theme_toggle/theme_toggle.rb rename to gem/lib/ruby_ui/theme_toggle/theme_toggle.rb diff --git a/gem/lib/ruby_ui/theme_toggle/theme_toggle_controller.js b/gem/lib/ruby_ui/theme_toggle/theme_toggle_controller.js new file mode 100644 index 00000000..01b3b24b --- /dev/null +++ b/gem/lib/ruby_ui/theme_toggle/theme_toggle_controller.js @@ -0,0 +1,30 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + initialize() { + this.setTheme() + } + + setTheme() { + // On page load or when changing themes, best to add inline in `head` to avoid FOUC + if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + document.documentElement.classList.add('dark') + document.documentElement.classList.remove('light') + } else { + document.documentElement.classList.remove('dark') + document.documentElement.classList.add('light') + } + } + + setLightTheme() { + // Whenever the user explicitly chooses light mode + localStorage.theme = 'light' + this.setTheme() + } + + setDarkTheme() { + // Whenever the user explicitly chooses dark mode + localStorage.theme = 'dark' + this.setTheme() + } +} diff --git a/gem/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb b/gem/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb new file mode 100644 index 00000000..1740a924 --- /dev/null +++ b/gem/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class Views::Docs::ThemeToggle < Views::Base + def view_template + component = "ThemeToggle" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Theme Toggle", description: "Toggle between dark/light theme.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "With icon", context: self) do + <<~RUBY + ThemeToggle do |toggle| + SetLightMode do + Button(variant: :ghost, icon: true) do + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-4 h-4" + ) do |s| + s.path( + d: + "M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z" + ) + end + end + end + + SetDarkMode do + Button(variant: :ghost, icon: true) do + svg( + xmlns: "http://www.w3.org/2000/svg", + viewbox: "0 0 24 24", + fill: "currentColor", + class: "w-4 h-4" + ) do |s| + s.path( + fill_rule: "evenodd", + d: + "M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6a9 9 0 009 9 8.97 8.97 0 003.463-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z", + clip_rule: "evenodd" + ) + end + end + end + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "With text", context: self) do + <<~RUBY + ThemeToggle do |toggle| + SetLightMode do + Button(variant: :primary) { "Light" } + end + + SetDarkMode do + Button(variant: :primary) { "Dark" } + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/lib/ruby_ui/tooltip/tooltip.rb b/gem/lib/ruby_ui/tooltip/tooltip.rb similarity index 100% rename from lib/ruby_ui/tooltip/tooltip.rb rename to gem/lib/ruby_ui/tooltip/tooltip.rb diff --git a/lib/ruby_ui/tooltip/tooltip_content.rb b/gem/lib/ruby_ui/tooltip/tooltip_content.rb similarity index 100% rename from lib/ruby_ui/tooltip/tooltip_content.rb rename to gem/lib/ruby_ui/tooltip/tooltip_content.rb diff --git a/gem/lib/ruby_ui/tooltip/tooltip_controller.js b/gem/lib/ruby_ui/tooltip/tooltip_controller.js new file mode 100644 index 00000000..20ba35ce --- /dev/null +++ b/gem/lib/ruby_ui/tooltip/tooltip_controller.js @@ -0,0 +1,38 @@ +import { Controller } from "@hotwired/stimulus"; +import { computePosition, autoUpdate, offset, shift } from "@floating-ui/dom"; + +export default class extends Controller { + static targets = ["trigger", "content"]; + static values = { placement: String } + + constructor(...args) { + super(...args); + this.cleanup; + } + + connect() { + this.setFloatingElement(); + + const tooltipId = this.contentTarget.getAttribute("id"); + this.triggerTarget.setAttribute("aria-describedby", tooltipId); + + } + + disconnect() { + this.cleanup(); + } + + setFloatingElement() { + this.cleanup = autoUpdate(this.triggerTarget, this.contentTarget, () => { + computePosition(this.triggerTarget, this.contentTarget, { + placement: this.placementValue, + middleware: [offset(4), shift()] + }).then(({ x, y }) => { + Object.assign(this.contentTarget.style, { + left: `${x}px`, + top: `${y}px`, + }); + }); + }); + } +} diff --git a/gem/lib/ruby_ui/tooltip/tooltip_docs.rb b/gem/lib/ruby_ui/tooltip/tooltip_docs.rb new file mode 100644 index 00000000..5189728b --- /dev/null +++ b/gem/lib/ruby_ui/tooltip/tooltip_docs.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class Views::Docs::Tooltip < Views::Base + def view_template + component = "Tooltip" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Tooltip", description: "A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.") + + Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "Example", context: self) do + <<~RUBY + Tooltip do + TooltipTrigger do + Button(variant: :outline, icon: true) do + bookmark_icon + end + end + TooltipContent do + Text { "Add to library" } + end + end + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end + + private + + def bookmark_icon + svg( + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewbox: "0 0 24 24", + stroke_width: "1.5", + stroke: "currentColor", + class: "w-4 h-4" + ) do |s| + s.path( + stroke_linecap: "round", + stroke_linejoin: "round", + d: + "M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z" + ) + end + end +end diff --git a/lib/ruby_ui/tooltip/tooltip_trigger.rb b/gem/lib/ruby_ui/tooltip/tooltip_trigger.rb similarity index 100% rename from lib/ruby_ui/tooltip/tooltip_trigger.rb rename to gem/lib/ruby_ui/tooltip/tooltip_trigger.rb diff --git a/lib/ruby_ui/typography/heading.rb b/gem/lib/ruby_ui/typography/heading.rb similarity index 100% rename from lib/ruby_ui/typography/heading.rb rename to gem/lib/ruby_ui/typography/heading.rb diff --git a/lib/ruby_ui/typography/inline_code.rb b/gem/lib/ruby_ui/typography/inline_code.rb similarity index 100% rename from lib/ruby_ui/typography/inline_code.rb rename to gem/lib/ruby_ui/typography/inline_code.rb diff --git a/lib/ruby_ui/typography/inline_link.rb b/gem/lib/ruby_ui/typography/inline_link.rb similarity index 100% rename from lib/ruby_ui/typography/inline_link.rb rename to gem/lib/ruby_ui/typography/inline_link.rb diff --git a/lib/ruby_ui/typography/text.rb b/gem/lib/ruby_ui/typography/text.rb similarity index 100% rename from lib/ruby_ui/typography/text.rb rename to gem/lib/ruby_ui/typography/text.rb diff --git a/lib/ruby_ui/typography/typography_blockquote.rb b/gem/lib/ruby_ui/typography/typography_blockquote.rb similarity index 100% rename from lib/ruby_ui/typography/typography_blockquote.rb rename to gem/lib/ruby_ui/typography/typography_blockquote.rb diff --git a/gem/lib/ruby_ui/typography/typography_docs.rb b/gem/lib/ruby_ui/typography/typography_docs.rb new file mode 100644 index 00000000..cf860dcf --- /dev/null +++ b/gem/lib/ruby_ui/typography/typography_docs.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +class Views::Docs::Typography < Views::Base + def view_template + component = "Typography" + + div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do + render Docs::Header.new(title: "Typography", description: "Sensible defaults to use for text.") + + Components.Heading(level: 2) { "Usage" } + + render Docs::VisualCodeExample.new(title: "h1", context: self) do + <<~RUBY + Heading(level: 1) { "This is an H1 title" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "h2", context: self) do + <<~RUBY + Heading(level: 2) { "This is an H2 title" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "h3", context: self) do + <<~RUBY + Heading(level: 3) { "This is an H3 title" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "h4", context: self) do + <<~RUBY + Heading(level: 4) { "This is an H4 title" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "p", context: self) do + <<~RUBY + Text { "This is an P tag" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Inline Link", context: self) do + <<~RUBY + Text(class: 'text-center') do + plain "Checkout our " + InlineLink(href: docs_installation_path) { "installation instructions" } + plain " to get started." + end + RUBY + end + + render Docs::VisualCodeExample.new(title: "List", context: self) do + <<~RUBY + Components.TypographyList(items: [ + 'Phlex is fast', + 'Phlex is easy to use', + 'Phlex is awesome', + ]) + RUBY + end + + render Docs::VisualCodeExample.new(title: "Numbered List", context: self) do + <<~RUBY + Components.TypographyList(items: [ + 'Copy', + 'Paste', + 'Customize', + ], numbered: true) + RUBY + end + + render Docs::VisualCodeExample.new(title: "Inline Code", context: self) do + <<~RUBY + InlineCode { "This is an inline code block" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Lead", context: self) do + <<~RUBY + Text(as: "p", size: "5", weight: "muted") { "A modal dialog that interrupts the user with important content and expects a response." } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Large", context: self) do + <<~RUBY + Text(size: "4", weight: "semibold") { "Are you sure absolutely sure?" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Small", context: self) do + <<~RUBY + Text(size: "sm") { "Email address" } + RUBY + end + + render Docs::VisualCodeExample.new(title: "Muted", context: self) do + <<~RUBY + Text(size: "2", class: "text-muted-foreground") { "Enter your email address." } + RUBY + end + + render Components::ComponentSetup::Tabs.new(component_name: component) + + render Docs::ComponentsTable.new(component_files(component)) + end + end +end diff --git a/package.json b/gem/package.json similarity index 100% rename from package.json rename to gem/package.json diff --git a/ruby_ui.gemspec b/gem/ruby_ui.gemspec similarity index 100% rename from ruby_ui.gemspec rename to gem/ruby_ui.gemspec diff --git a/gem/test/gemspec_test.rb b/gem/test/gemspec_test.rb new file mode 100644 index 00000000..53b23c0b --- /dev/null +++ b/gem/test/gemspec_test.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "test_helper" +require "rubygems/package" +require "tmpdir" + +class GemspecTest < Minitest::Test + GEM_ROOT = File.expand_path("..", __dir__) + + def test_packaged_files_match_allowlist + files = build_and_list_files + + refute_empty files, "expected the .gem to ship some files" + + offenders = files.reject { |f| allowed?(f) } + assert_empty offenders, <<~MSG + ruby_ui.gemspec packaged unexpected files. The gemspec's + s.files allowlist must keep these out of the published gem + (e.g. files from docs/, test/, .github/, Gemfile, etc.): + + #{offenders.join("\n ")} + MSG + end + + def test_includes_readme_and_license_and_lib + files = build_and_list_files + + assert_includes files, "README.md" + assert_includes files, "LICENSE.txt" + assert files.any? { |f| f.start_with?("lib/") }, "expected lib/ files in the gem" + end + + private + + def allowed?(path) + path == "README.md" || path == "LICENSE.txt" || path.start_with?("lib/") + end + + def build_and_list_files + Dir.mktmpdir do |tmpdir| + gem_path = File.join(tmpdir, "ruby_ui.gem") + Dir.chdir(GEM_ROOT) do + output = `gem build ruby_ui.gemspec --output #{gem_path} 2>&1` + raise "gem build failed:\n#{output}" unless $?.success? + end + Gem::Package.new(gem_path).contents + end + end +end diff --git a/test/generators/component_generator_test.rb b/gem/test/generators/component_generator_test.rb similarity index 100% rename from test/generators/component_generator_test.rb rename to gem/test/generators/component_generator_test.rb diff --git a/test/ruby_ui/accordion_test.rb b/gem/test/ruby_ui/accordion_test.rb similarity index 100% rename from test/ruby_ui/accordion_test.rb rename to gem/test/ruby_ui/accordion_test.rb diff --git a/test/ruby_ui/alert_dialog_test.rb b/gem/test/ruby_ui/alert_dialog_test.rb similarity index 100% rename from test/ruby_ui/alert_dialog_test.rb rename to gem/test/ruby_ui/alert_dialog_test.rb diff --git a/test/ruby_ui/alert_test.rb b/gem/test/ruby_ui/alert_test.rb similarity index 100% rename from test/ruby_ui/alert_test.rb rename to gem/test/ruby_ui/alert_test.rb diff --git a/test/ruby_ui/aspect_ratio_test.rb b/gem/test/ruby_ui/aspect_ratio_test.rb similarity index 100% rename from test/ruby_ui/aspect_ratio_test.rb rename to gem/test/ruby_ui/aspect_ratio_test.rb diff --git a/test/ruby_ui/avatar_test.rb b/gem/test/ruby_ui/avatar_test.rb similarity index 100% rename from test/ruby_ui/avatar_test.rb rename to gem/test/ruby_ui/avatar_test.rb diff --git a/test/ruby_ui/badge_test.rb b/gem/test/ruby_ui/badge_test.rb similarity index 100% rename from test/ruby_ui/badge_test.rb rename to gem/test/ruby_ui/badge_test.rb diff --git a/test/ruby_ui/breadcrumb_test.rb b/gem/test/ruby_ui/breadcrumb_test.rb similarity index 100% rename from test/ruby_ui/breadcrumb_test.rb rename to gem/test/ruby_ui/breadcrumb_test.rb diff --git a/test/ruby_ui/button_test.rb b/gem/test/ruby_ui/button_test.rb similarity index 100% rename from test/ruby_ui/button_test.rb rename to gem/test/ruby_ui/button_test.rb diff --git a/test/ruby_ui/calendar_test.rb b/gem/test/ruby_ui/calendar_test.rb similarity index 100% rename from test/ruby_ui/calendar_test.rb rename to gem/test/ruby_ui/calendar_test.rb diff --git a/test/ruby_ui/card_test.rb b/gem/test/ruby_ui/card_test.rb similarity index 100% rename from test/ruby_ui/card_test.rb rename to gem/test/ruby_ui/card_test.rb diff --git a/test/ruby_ui/carousel_test.rb b/gem/test/ruby_ui/carousel_test.rb similarity index 100% rename from test/ruby_ui/carousel_test.rb rename to gem/test/ruby_ui/carousel_test.rb diff --git a/test/ruby_ui/chart_test.rb b/gem/test/ruby_ui/chart_test.rb similarity index 100% rename from test/ruby_ui/chart_test.rb rename to gem/test/ruby_ui/chart_test.rb diff --git a/test/ruby_ui/checkbox_test.rb b/gem/test/ruby_ui/checkbox_test.rb similarity index 100% rename from test/ruby_ui/checkbox_test.rb rename to gem/test/ruby_ui/checkbox_test.rb diff --git a/test/ruby_ui/clipboard_test.rb b/gem/test/ruby_ui/clipboard_test.rb similarity index 100% rename from test/ruby_ui/clipboard_test.rb rename to gem/test/ruby_ui/clipboard_test.rb diff --git a/test/ruby_ui/codeblock_test.rb b/gem/test/ruby_ui/codeblock_test.rb similarity index 100% rename from test/ruby_ui/codeblock_test.rb rename to gem/test/ruby_ui/codeblock_test.rb diff --git a/test/ruby_ui/collapsible_test.rb b/gem/test/ruby_ui/collapsible_test.rb similarity index 100% rename from test/ruby_ui/collapsible_test.rb rename to gem/test/ruby_ui/collapsible_test.rb diff --git a/test/ruby_ui/combobox_test.rb b/gem/test/ruby_ui/combobox_test.rb similarity index 100% rename from test/ruby_ui/combobox_test.rb rename to gem/test/ruby_ui/combobox_test.rb diff --git a/test/ruby_ui/command_test.rb b/gem/test/ruby_ui/command_test.rb similarity index 100% rename from test/ruby_ui/command_test.rb rename to gem/test/ruby_ui/command_test.rb diff --git a/test/ruby_ui/context_menu_test.rb b/gem/test/ruby_ui/context_menu_test.rb similarity index 100% rename from test/ruby_ui/context_menu_test.rb rename to gem/test/ruby_ui/context_menu_test.rb diff --git a/test/ruby_ui/dialog_test.rb b/gem/test/ruby_ui/dialog_test.rb similarity index 100% rename from test/ruby_ui/dialog_test.rb rename to gem/test/ruby_ui/dialog_test.rb diff --git a/test/ruby_ui/dropdown_menu_test.rb b/gem/test/ruby_ui/dropdown_menu_test.rb similarity index 100% rename from test/ruby_ui/dropdown_menu_test.rb rename to gem/test/ruby_ui/dropdown_menu_test.rb diff --git a/test/ruby_ui/form_test.rb b/gem/test/ruby_ui/form_test.rb similarity index 100% rename from test/ruby_ui/form_test.rb rename to gem/test/ruby_ui/form_test.rb diff --git a/test/ruby_ui/hover_card_test.rb b/gem/test/ruby_ui/hover_card_test.rb similarity index 100% rename from test/ruby_ui/hover_card_test.rb rename to gem/test/ruby_ui/hover_card_test.rb diff --git a/test/ruby_ui/inline_code_test.rb b/gem/test/ruby_ui/inline_code_test.rb similarity index 100% rename from test/ruby_ui/inline_code_test.rb rename to gem/test/ruby_ui/inline_code_test.rb diff --git a/test/ruby_ui/inline_link_test.rb b/gem/test/ruby_ui/inline_link_test.rb similarity index 100% rename from test/ruby_ui/inline_link_test.rb rename to gem/test/ruby_ui/inline_link_test.rb diff --git a/test/ruby_ui/input_test.rb b/gem/test/ruby_ui/input_test.rb similarity index 100% rename from test/ruby_ui/input_test.rb rename to gem/test/ruby_ui/input_test.rb diff --git a/test/ruby_ui/link_test.rb b/gem/test/ruby_ui/link_test.rb similarity index 100% rename from test/ruby_ui/link_test.rb rename to gem/test/ruby_ui/link_test.rb diff --git a/test/ruby_ui/masked_input_test.rb b/gem/test/ruby_ui/masked_input_test.rb similarity index 100% rename from test/ruby_ui/masked_input_test.rb rename to gem/test/ruby_ui/masked_input_test.rb diff --git a/test/ruby_ui/native_select_test.rb b/gem/test/ruby_ui/native_select_test.rb similarity index 100% rename from test/ruby_ui/native_select_test.rb rename to gem/test/ruby_ui/native_select_test.rb diff --git a/test/ruby_ui/pagination_test.rb b/gem/test/ruby_ui/pagination_test.rb similarity index 100% rename from test/ruby_ui/pagination_test.rb rename to gem/test/ruby_ui/pagination_test.rb diff --git a/test/ruby_ui/popover_test.rb b/gem/test/ruby_ui/popover_test.rb similarity index 100% rename from test/ruby_ui/popover_test.rb rename to gem/test/ruby_ui/popover_test.rb diff --git a/test/ruby_ui/progress_test.rb b/gem/test/ruby_ui/progress_test.rb similarity index 100% rename from test/ruby_ui/progress_test.rb rename to gem/test/ruby_ui/progress_test.rb diff --git a/test/ruby_ui/select_test.rb b/gem/test/ruby_ui/select_test.rb similarity index 100% rename from test/ruby_ui/select_test.rb rename to gem/test/ruby_ui/select_test.rb diff --git a/test/ruby_ui/separator_test.rb b/gem/test/ruby_ui/separator_test.rb similarity index 100% rename from test/ruby_ui/separator_test.rb rename to gem/test/ruby_ui/separator_test.rb diff --git a/test/ruby_ui/sheet_test.rb b/gem/test/ruby_ui/sheet_test.rb similarity index 100% rename from test/ruby_ui/sheet_test.rb rename to gem/test/ruby_ui/sheet_test.rb diff --git a/test/ruby_ui/shortcut_key_test.rb b/gem/test/ruby_ui/shortcut_key_test.rb similarity index 100% rename from test/ruby_ui/shortcut_key_test.rb rename to gem/test/ruby_ui/shortcut_key_test.rb diff --git a/test/ruby_ui/sidebar_test.rb b/gem/test/ruby_ui/sidebar_test.rb similarity index 100% rename from test/ruby_ui/sidebar_test.rb rename to gem/test/ruby_ui/sidebar_test.rb diff --git a/test/ruby_ui/skeleton_test.rb b/gem/test/ruby_ui/skeleton_test.rb similarity index 100% rename from test/ruby_ui/skeleton_test.rb rename to gem/test/ruby_ui/skeleton_test.rb diff --git a/test/ruby_ui/switch_test.rb b/gem/test/ruby_ui/switch_test.rb similarity index 100% rename from test/ruby_ui/switch_test.rb rename to gem/test/ruby_ui/switch_test.rb diff --git a/test/ruby_ui/table_test.rb b/gem/test/ruby_ui/table_test.rb similarity index 100% rename from test/ruby_ui/table_test.rb rename to gem/test/ruby_ui/table_test.rb diff --git a/test/ruby_ui/tabs_test.rb b/gem/test/ruby_ui/tabs_test.rb similarity index 100% rename from test/ruby_ui/tabs_test.rb rename to gem/test/ruby_ui/tabs_test.rb diff --git a/test/ruby_ui/text_test.rb b/gem/test/ruby_ui/text_test.rb similarity index 100% rename from test/ruby_ui/text_test.rb rename to gem/test/ruby_ui/text_test.rb diff --git a/test/ruby_ui/textarea_test.rb b/gem/test/ruby_ui/textarea_test.rb similarity index 100% rename from test/ruby_ui/textarea_test.rb rename to gem/test/ruby_ui/textarea_test.rb diff --git a/test/ruby_ui/theme_toggle_test.rb b/gem/test/ruby_ui/theme_toggle_test.rb similarity index 100% rename from test/ruby_ui/theme_toggle_test.rb rename to gem/test/ruby_ui/theme_toggle_test.rb diff --git a/test/ruby_ui/tooltip_test.rb b/gem/test/ruby_ui/tooltip_test.rb similarity index 100% rename from test/ruby_ui/tooltip_test.rb rename to gem/test/ruby_ui/tooltip_test.rb diff --git a/test/test_helper.rb b/gem/test/test_helper.rb similarity index 100% rename from test/test_helper.rb rename to gem/test/test_helper.rb diff --git a/yarn.lock b/gem/yarn.lock similarity index 100% rename from yarn.lock rename to gem/yarn.lock