From 8759645c5ff53c03dca1a07c88c0d089bedcd1c0 Mon Sep 17 00:00:00 2001 From: Brett Chalupa Date: Wed, 15 Oct 2025 10:22:27 -0400 Subject: [PATCH 1/3] Add support for running as a Rack app This makes it so that users of the gem can optionally run the app as a Rack app, making it easy to mount within a Rails or Sinatra app. Reasons why would be to add authentication or have a nicer developer experience instead of regenerating the docs over and over again. Fixes https://github.com/brettchalupa/graphql-docs/issues/102 --- README.md | 143 +++++- Rakefile | 30 ++ config.ru | 44 ++ graphql-docs.gemspec | 8 + lib/graphql-docs.rb | 7 + lib/graphql-docs/app.rb | 434 ++++++++++++++++++ lib/graphql-docs/generator.rb | 3 +- lib/graphql-docs/helpers.rb | 12 +- .../layouts/assets/_sass/_sidebar.scss | 5 +- .../layouts/assets/images/search.svg | 3 - .../layouts/includes/sidebar.html | 4 +- lib/graphql-docs/renderer.rb | 3 +- test/graphql-docs/app_test.rb | 210 +++++++++ test/graphql-docs/generator_test.rb | 45 ++ test/graphql-docs/renderer_test.rb | 17 + 15 files changed, 949 insertions(+), 19 deletions(-) create mode 100644 config.ru create mode 100644 lib/graphql-docs/app.rb delete mode 100644 lib/graphql-docs/layouts/assets/images/search.svg create mode 100644 test/graphql-docs/app_test.rb diff --git a/README.md b/README.md index f367df48..283e6b78 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,13 @@ gem install graphql-docs ## Usage +GraphQLDocs provides two ways to serve your documentation: + +1. **Static Site Generator (SSG)** - Pre-generate all HTML files (default) +2. **Rack Application** - Serve documentation dynamically on-demand + +### Static Site Generation (SSG) + GraphQLDocs can be used as a Ruby library to build the documentation website. Using it as a Ruby library allows for more control and using every supported option. Here's an example: ```ruby @@ -55,6 +62,110 @@ See all of the supported CLI options with: graphql-docs -h ``` +### Rack Application (Dynamic) + +For more flexibility and control, you can serve documentation dynamically using the Rack application. This is useful for: + +- Internal tools with frequently changing schemas +- Integration with existing Rails/Sinatra applications +- Adding authentication/authorization middleware +- Dynamic schema loading from databases or APIs + +**Requirements**: The Rack application feature requires the `rack` gem (version 2.x or 3.x). Add it to your Gemfile: + +```ruby +gem 'rack', '~> 3.0' # or '~> 2.0' for Rack 2.x +``` + +The gem is compatible with both Rack 2.x and 3.x, so you can use whichever version your application requires. + +#### Standalone Rack App + +Create a `config.ru` file: + +```ruby +require 'graphql-docs' + +schema = File.read('schema.graphql') + +app = GraphQLDocs::App.new( + schema: schema, + options: { + base_url: '', + use_default_styles: true, + cache: true # Enable page caching + } +) + +run app +``` + +Then run with: + +```console +rackup config.ru +``` + +Visit `http://localhost:9292` to view your docs. + +#### Mounting in Rails + +```ruby +# config/routes.rb +require 'graphql-docs' + +Rails.application.routes.draw do + mount GraphQLDocs::App.new(schema: MyGraphQLSchema) => '/docs' +end +``` + +#### Mounting in Sinatra + +```ruby +require 'sinatra' +require 'graphql-docs' + +schema = File.read('schema.graphql') +docs_app = GraphQLDocs::App.new(schema: schema) + +map '/docs' do + run docs_app +end + +map '/' do + run Sinatra::Application +end +``` + +#### Rack App Features + +- **On-demand generation** - Pages are generated when requested +- **Built-in caching** - Generated pages are cached in memory (disable with `cache: false`) +- **Schema reloading** - Update schema without restarting server: + +```ruby +app = GraphQLDocs::App.new(schema: schema) + +# Later, reload with new schema +new_schema = File.read('updated_schema.graphql') +app.reload_schema!(new_schema) +``` + +- **Asset serving** - CSS, fonts, and images served automatically +- **Error handling** - Friendly error pages for missing types + +#### SSG vs Rack Comparison + +| Feature | SSG | Rack App | +|---------|-----|----------| +| Setup complexity | Low | Medium | +| First page load | Instant | Fast (with caching) | +| Schema updates | Manual rebuild | Automatic/reload | +| Hosting | Any static host | Requires Ruby server | +| Memory usage | Minimal | Higher (cached pages) | +| Authentication | Separate layer | Built-in middleware | +| Best for | Public docs, open source | Internal tools, dynamic schemas | + ## Breakdown There are several phases going on the single `GraphQLDocs.build` call: @@ -398,20 +509,42 @@ an interactive prompt that will allow you to experiment. ## Sample Site -Clone this repository and run: +Clone this repository and try out both modes: -``` +### Static Site Generation + +Generate the sample documentation to the `output` directory: + +```console bin/rake sample:generate ``` -to see some sample output in the `output` dir. +Then boot up a server to view the pre-generated files: + +```console +bin/rake sample:serve +``` + +Visit `http://localhost:5050` to view the static documentation. -Boot up a server to view it: +### Rack Application (Dynamic) +Run the sample docs as a dynamic Rack application: + +```console +bin/rake sample:rack ``` -bin/rake sample:serve + +Or use the config.ru directly: + +```console +rackup config.ru ``` +Visit `http://localhost:9292` to view the documentation served dynamically. + +**Key Difference**: The SSG version pre-generates all pages (faster initial load, no runtime cost), while the Rack version generates pages on-demand (better for dynamic schemas, easier integration). + ## Credits Originally built by [gjtorikian](https://github.com/gjtorikian). Actively maintained by [brettchalupa](https://github.com/brettchalupa). diff --git a/Rakefile b/Rakefile index 11a728fd..00e322af 100644 --- a/Rakefile +++ b/Rakefile @@ -83,4 +83,34 @@ namespace :sample do server.start end task server: :serve + + desc 'Run the sample docs as a Rack application (dynamic, on-demand generation)' + task :rack do + require 'rack' + require 'graphql-docs' + + schema_path = File.join(File.dirname(__FILE__), 'test', 'graphql-docs', 'fixtures', 'gh-schema.graphql') + schema = File.read(schema_path) + + app = GraphQLDocs::App.new( + schema: schema, + options: { + base_url: '', + use_default_styles: true, + cache: true + } + ) + + PORT = ENV.fetch('PORT', '9292') + puts "Starting Rack server in dynamic mode (on-demand generation)" + puts "Navigate to http://localhost:#{PORT} to view the sample docs" + puts "Press Ctrl+C to stop" + puts "" + puts "NOTE: This serves documentation dynamically - pages are generated on request" + puts " Compare with 'rake sample:serve' which serves pre-generated static files" + puts "" + + # Use rackup for Rack 3.x compatibility + sh "rackup config.ru -p #{PORT}" + end end diff --git a/config.ru b/config.ru new file mode 100644 index 00000000..12877bea --- /dev/null +++ b/config.ru @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Rack configuration file for running GraphQL Docs as a web server +# +# This demonstrates using GraphQLDocs as a Rack application that serves +# documentation dynamically on-demand instead of pre-generating static files. +# +# Run with: rackup config.ru +# Or with specific port: rackup config.ru -p 9292 + +require_relative 'lib/graphql-docs' + +# Load the sample GraphQL schema +schema_path = File.join(__dir__, 'test', 'graphql-docs', 'fixtures', 'gh-schema.graphql') + +unless File.exist?(schema_path) + puts "Error: Sample schema not found at #{schema_path}" + puts "Please ensure the schema file exists before starting the server." + exit 1 +end + +schema = File.read(schema_path) + +# Create the Rack app +app = GraphQLDocs::App.new( + schema: schema, + options: { + base_url: '', + use_default_styles: true, + cache: true + } +) + +# Log requests in development +use Rack::CommonLogger + +# Add reloader for development (optional, requires 'rack' gem) +if ENV['RACK_ENV'] != 'production' + puts "Running in development mode" + puts "Visit http://localhost:9292 to view the documentation" + puts "Press Ctrl+C to stop the server" +end + +run app diff --git a/graphql-docs.gemspec b/graphql-docs.gemspec index 7eedf425..20908819 100644 --- a/graphql-docs.gemspec +++ b/graphql-docs.gemspec @@ -50,9 +50,17 @@ Gem::Specification.new do |spec| spec.add_dependency 'ostruct', '~> 0.6' spec.add_dependency 'logger', '~> 1.6' + # rack application support (optional, only needed for GraphQLDocs::App) + # Users can install rack separately if they want to use the Rack app feature: + # gem 'rack', '~> 2.0' or gem 'rack', '~> 3.0' + # The gem works with both Rack 2.x and 3.x + spec.add_development_dependency 'html-proofer', '~> 3.4' spec.add_development_dependency 'minitest', '~> 5.24' spec.add_development_dependency 'minitest-focus', '~> 1.1' + spec.add_development_dependency 'rack', '>= 2.0', '< 4' + spec.add_development_dependency 'rack-test', '~> 2.0' + spec.add_development_dependency 'rackup', '~> 2.0' spec.add_development_dependency 'rake', '~> 13.0' spec.add_development_dependency 'rubocop', '~> 1.37' spec.add_development_dependency 'rubocop-performance', '~> 1.15' diff --git a/lib/graphql-docs.rb b/lib/graphql-docs.rb index 3dfcf5f4..320c04e9 100644 --- a/lib/graphql-docs.rb +++ b/lib/graphql-docs.rb @@ -7,6 +7,13 @@ require 'graphql-docs/parser' require 'graphql-docs/version' +# Lazy-load the Rack app - only loads if Rack is available +begin + require 'graphql-docs/app' if defined?(Rack) +rescue LoadError + # Rack not available, App class won't be loaded +end + # GraphQLDocs is a library for generating beautiful HTML documentation from GraphQL schemas. # It parses GraphQL schema files or schema objects and generates a complete documentation website # with customizable templates and styling. diff --git a/lib/graphql-docs/app.rb b/lib/graphql-docs/app.rb new file mode 100644 index 00000000..5ec0cddd --- /dev/null +++ b/lib/graphql-docs/app.rb @@ -0,0 +1,434 @@ +# frozen_string_literal: true + +require 'erb' + +# Lazy-load Rack when the App class is first instantiated +begin + require 'rack' +rescue LoadError => e + # Define a stub that will raise a better error message + module Rack + def self.const_missing(name) + raise LoadError, "The GraphQLDocs::App feature requires the 'rack' gem. " \ + "Please add it to your Gemfile: gem 'rack', '~> 2.0' or gem 'rack', '~> 3.0'" + end + end +end + +module GraphQLDocs + # Rack application for serving GraphQL documentation on-demand. + # + # This provides an alternative to the static site generator approach, allowing + # documentation to be served dynamically from a Rack-compatible web server. + # Pages are generated on-demand and can be cached for performance. + # + # @example Standalone usage + # app = GraphQLDocs::App.new( + # schema: 'type Query { hello: String }', + # options: { base_url: '' } + # ) + # run app + # + # @example Mount in Rails + # mount GraphQLDocs::App.new(schema: MySchema) => '/docs' + # + # @example With caching + # app = GraphQLDocs::App.new( + # schema: schema_string, + # options: { cache: true } + # ) + class App + include Helpers + + # @!attribute [r] parsed_schema + # @return [Hash] The parsed GraphQL schema structure + attr_reader :parsed_schema + + # @!attribute [r] options + # @return [Hash] Configuration options for the app + attr_reader :options + + # @!attribute [r] base_options + # @return [Hash] Base configuration options (immutable) + attr_reader :base_options + + # Initializes a new Rack app instance. + # + # @param schema [String, GraphQL::Schema] GraphQL schema as IDL string or schema class + # @param filename [String, nil] Path to GraphQL schema file (alternative to schema param) + # @param options [Hash] Configuration options + # @option options [String] :base_url ('') Base URL prefix for all routes + # @option options [Boolean] :cache (true) Enable page caching + # @option options [Boolean] :use_default_styles (true) Serve default CSS + # @option options [Hash] :templates Custom template paths + # @option options [Class] :renderer Custom renderer class + # + # @raise [ArgumentError] If neither schema nor filename is provided + def initialize(schema: nil, filename: nil, options: {}) + raise ArgumentError, 'Must provide either schema or filename' if schema.nil? && filename.nil? + + @base_options = Configuration::GRAPHQLDOCS_DEFAULTS.merge(options).freeze + @options = @base_options.dup + + # Load schema from file if filename provided + if filename + raise ArgumentError, "#{filename} does not exist!" unless File.exist?(filename) + schema = File.read(filename) + end + + # Parse schema once at initialization + parser = Parser.new(schema, @options) + @parsed_schema = parser.parse + + @renderer = @options[:renderer].new(@parsed_schema, @options) + + # Initialize cache + @cache_enabled = @options.fetch(:cache, true) + @cache = {} if @cache_enabled + + # Load templates + load_templates + + # Pre-compile assets if using default styles + compile_assets if @options[:use_default_styles] + end + + # Rack interface method. + # + # @param env [Hash] Rack environment hash + # @return [Array] Rack response tuple [status, headers, body] + def call(env) + request = Rack::Request.new(env) + path = clean_path(request.path_info) + + route(path) + rescue StandardError => e + [500, { 'content-type' => 'text/html' }, [error_page(e)]] + end + + # Clears the page cache. + # + # @return [void] + def clear_cache! + @cache&.clear + end + + # Reloads the schema and clears cache. + # + # @param new_schema [String, GraphQL::Schema] New schema to parse + # @return [void] + def reload_schema!(new_schema) + parser = Parser.new(new_schema, @options) + @parsed_schema = parser.parse + @renderer = @options[:renderer].new(@parsed_schema, @options) + clear_cache! + end + + private + + def clean_path(path) + # Remove base_url prefix if present + base = @options[:base_url] + path = path.sub(/^#{Regexp.escape(base)}/, '') if base && !base.empty? + + # Normalize path + path = '/' if path.empty? + path.sub(/\/$/, '') # Remove trailing slash + end + + def route(path) + case path + when '', '/', '/index.html', '/index' + serve_landing_page(:index) + when '/operation/query', '/operation/query/index.html', '/operation/query/index' + serve_operation_page + when '/operation/mutation', '/operation/mutation/index.html', '/operation/mutation/index' + serve_mutation_operation_page + when %r{^/object/([^/]+)(?:/index(?:\.html)?)?$} + serve_type_page(:object, $1) + when %r{^/query/([^/]+)(?:/index(?:\.html)?)?$} + serve_type_page(:query, $1) + when %r{^/mutation/([^/]+)(?:/index(?:\.html)?)?$} + serve_type_page(:mutation, $1) + when %r{^/interface/([^/]+)(?:/index(?:\.html)?)?$} + serve_type_page(:interface, $1) + when %r{^/enum/([^/]+)(?:/index(?:\.html)?)?$} + serve_type_page(:enum, $1) + when %r{^/union/([^/]+)(?:/index(?:\.html)?)?$} + serve_type_page(:union, $1) + when %r{^/input_object/([^/]+)(?:/index(?:\.html)?)?$} + serve_type_page(:input_object, $1) + when %r{^/scalar/([^/]+)(?:/index(?:\.html)?)?$} + serve_type_page(:scalar, $1) + when %r{^/directive/([^/]+)(?:/index(?:\.html)?)?$} + serve_type_page(:directive, $1) + when %r{^/assets/(.+)$} + serve_asset($1) + else + [404, { 'content-type' => 'text/html' }, [not_found_page(path)]] + end + end + + def serve_landing_page(page_type) + cache_key = "landing:#{page_type}" + + content = fetch_from_cache(cache_key) do + generate_landing_page(page_type) + end + + return [404, { 'content-type' => 'text/html' }, ['Landing page not found']] if content.nil? + + [200, { 'content-type' => 'text/html; charset=utf-8' }, [content]] + end + + def serve_operation_page + cache_key = 'operation:query' + + content = fetch_from_cache(cache_key) do + query_type = graphql_operation_types.find { |qt| qt[:name] == graphql_root_types['query'] } + return nil unless query_type + + generate_type_content(:operations, query_type, 'operation', 'query') + end + + return [404, { 'content-type' => 'text/html' }, ['Query type not found']] if content.nil? + + [200, { 'content-type' => 'text/html; charset=utf-8' }, [content]] + end + + def serve_mutation_operation_page + cache_key = 'operation:mutation' + + content = fetch_from_cache(cache_key) do + mutation_type = graphql_operation_types.find { |mt| mt[:name] == graphql_root_types['mutation'] } + return nil unless mutation_type + + generate_type_content(:operations, mutation_type, 'operation', 'mutation') + end + + return [404, { 'content-type' => 'text/html' }, ['Mutation type not found']] if content.nil? + + [200, { 'content-type' => 'text/html; charset=utf-8' }, [content]] + end + + def serve_type_page(type, name) + name_lower = name.downcase + cache_key = "#{type}:#{name_lower}" + + content = fetch_from_cache(cache_key) do + generate_page_for_type(type, name) + end + + return [404, { 'content-type' => 'text/html' }, ["#{type.capitalize} '#{name}' not found"]] if content.nil? + + [200, { 'content-type' => 'text/html; charset=utf-8' }, [content]] + end + + def serve_asset(asset_path) + # Serve compiled CSS + if asset_path == 'style.css' && @compiled_css + return [200, { 'content-type' => 'text/css; charset=utf-8' }, [@compiled_css]] + end + + # Serve static assets from layouts/assets directory + asset_file = File.join(File.dirname(__FILE__), 'layouts', 'assets', asset_path) + + if File.exist?(asset_file) && File.file?(asset_file) + content = File.read(asset_file) + content_type = mime_type_for(asset_path) + [200, { 'content-type' => content_type }, [content]] + else + [404, { 'content-type' => 'text/plain' }, ['Asset not found']] + end + end + + def generate_landing_page(page_type) + landing_page_var = instance_variable_get("@#{page_type}_landing_page") + return nil if landing_page_var.nil? + + render_content(landing_page_var, type_category: 'static', type_name: page_type.to_s) + end + + def generate_page_for_type(type, name) + collection = case type + when :object then graphql_object_types + when :query then graphql_query_types + when :mutation then graphql_mutation_types + when :interface then graphql_interface_types + when :enum then graphql_enum_types + when :union then graphql_union_types + when :input_object then graphql_input_object_types + when :scalar then graphql_scalar_types + when :directive then graphql_directive_types + else return nil + end + + # Find the type (case-insensitive) + type_data = collection.find { |t| t[:name].downcase == name.downcase } + return nil unless type_data + + template_key = case type + when :object then :objects + when :query then :queries + when :mutation then :mutations + when :interface then :interfaces + when :enum then :enums + when :union then :unions + when :input_object then :input_objects + when :scalar then :scalars + when :directive then :directives + end + + generate_type_content(template_key, type_data, type.to_s, name) + end + + def generate_type_content(template_key, type_data, type_category, type_name) + template = instance_variable_get("@#{template_key}_template") + return nil unless template + + opts = @options.merge(type: type_data).merge(helper_methods) + contents = template.result(OpenStruct.new(opts).instance_eval { binding }) + + # Normalize spacing + contents.gsub!(/^\s+$/, '') + contents.gsub!(/^\s{4}/m, ' ') + + render_content(contents, type_category: type_category, type_name: type_name) + end + + def render_content(contents, type_category:, type_name:) + # Parse YAML frontmatter if present (similar to generator.rb write_file) + if yaml?(contents) + meta, contents = split_into_metadata_and_contents(contents) + # Temporarily merge metadata into options for this render + # Need to mutate in place so renderer sees the changes + @options.merge!(meta) + result = @renderer.render(contents, type: type_category, name: type_name, filename: nil) + # Reset options by clearing and repopulating from base (in place) + @options.clear + @options.merge!(@base_options) + result + else + @renderer.render(contents, type: type_category, name: type_name, filename: nil) + end + end + + def fetch_from_cache(key) + return yield unless @cache_enabled + + if @cache.key?(key) + @cache[key] + else + result = yield + @cache[key] = result if result + result + end + end + + def load_templates + # Load type templates + %i[operations objects queries mutations interfaces enums unions input_objects scalars directives].each do |sym| + template_file = @options[:templates][sym] + next unless File.exist?(template_file) + + instance_variable_set("@#{sym}_template", ERB.new(File.read(template_file))) + end + + # Load landing pages + %i[index object query mutation interface enum union input_object scalar directive].each do |sym| + landing_page_file = @options[:landing_pages][sym] + next if landing_page_file.nil? || !File.exist?(landing_page_file) + + landing_page_contents = File.read(landing_page_file) + metadata = '' + + if File.extname(landing_page_file) == '.erb' + opts = @options.merge(@options[:landing_pages][:variables]).merge(helper_methods) + if yaml?(landing_page_contents) + metadata, landing_page = split_into_metadata_and_contents(landing_page_contents, parse: false) + erb_template = ERB.new(landing_page) + else + erb_template = ERB.new(landing_page_contents) + end + landing_page_contents = erb_template.result(OpenStruct.new(opts).instance_eval { binding }) + end + + instance_variable_set("@#{sym}_landing_page", metadata + landing_page_contents) + end + end + + def compile_assets + return unless @options[:use_default_styles] + + assets_dir = File.join(File.dirname(__FILE__), 'layouts', 'assets') + scss_file = File.join(assets_dir, 'css', 'screen.scss') + + if File.exist?(scss_file) + require 'sass-embedded' + @compiled_css = Sass.compile(scss_file).css + end + end + + def mime_type_for(path) + ext = File.extname(path).downcase + case ext + when '.css' then 'text/css' + when '.js' then 'application/javascript' + when '.png' then 'image/png' + when '.jpg', '.jpeg' then 'image/jpeg' + when '.gif' then 'image/gif' + when '.svg' then 'image/svg+xml' + when '.woff' then 'font/woff' + when '.woff2' then 'font/woff2' + when '.ttf' then 'font/ttf' + when '.eot' then 'application/vnd.ms-fontobject' + else 'application/octet-stream' + end + end + + def not_found_page(path) + <<~HTML + + + + 404 Not Found + + + +

404 Not Found

+

The requested path #{Rack::Utils.escape_html(path)} was not found.

+

← Back to documentation home

+ + + HTML + end + + def error_page(error) + <<~HTML + + + + 500 Internal Server Error + + + +

500 Internal Server Error

+

An error occurred while generating the documentation page.

+

Error Details:

+
#{Rack::Utils.escape_html(error.class.name)}: #{Rack::Utils.escape_html(error.message)}
+
+#{Rack::Utils.escape_html(error.backtrace.first(10).join("\n"))}
+ + + HTML + end + end +end diff --git a/lib/graphql-docs/generator.rb b/lib/graphql-docs/generator.rb index e98f19ef..3774f3be 100644 --- a/lib/graphql-docs/generator.rb +++ b/lib/graphql-docs/generator.rb @@ -292,7 +292,8 @@ def write_file(type, name, contents, trim: true) if yaml?(contents) # Split data meta, contents = split_into_metadata_and_contents(contents) - @options = @options.merge(meta) + # Use merge! to mutate in place so renderer sees the changes + @options.merge!(meta) end if trim diff --git a/lib/graphql-docs/helpers.rb b/lib/graphql-docs/helpers.rb index 5fc79bc4..1933aaad 100644 --- a/lib/graphql-docs/helpers.rb +++ b/lib/graphql-docs/helpers.rb @@ -18,9 +18,9 @@ module Helpers # Matches all characters that are not alphanumeric or common URL-safe characters. SLUGIFY_PRETTY_REGEXP = Regexp.new("[^[:alnum:]._~!$&'()+,;=@]+").freeze - # @!attribute [rw] templates - # @return [Hash] Cache of loaded ERB templates - attr_accessor :templates + # @!attribute [rw] included_templates + # @return [Hash] Cache of loaded ERB templates for includes + attr_accessor :included_templates # Converts a string into a URL-friendly slug. # @@ -189,13 +189,13 @@ def yaml_split(contents) private def fetch_include(filename) - @templates ||= {} + @included_templates ||= {} - return @templates[filename] unless @templates[filename].nil? + return @included_templates[filename] unless @included_templates[filename].nil? contents = File.read(File.join(@options[:templates][:includes], filename)) - @templates[filename] = ERB.new(contents) + @included_templates[filename] = ERB.new(contents) end def helper_methods diff --git a/lib/graphql-docs/layouts/assets/_sass/_sidebar.scss b/lib/graphql-docs/layouts/assets/_sass/_sidebar.scss index a2d1821c..acd5ca61 100644 --- a/lib/graphql-docs/layouts/assets/_sass/_sidebar.scss +++ b/lib/graphql-docs/layouts/assets/_sass/_sidebar.scss @@ -78,11 +78,12 @@ padding: 0.01em 16px; margin-bottom: 20px; - img { + svg { position: absolute; left: 10px; height: 16px; width: 16px; + fill: currentColor; } input { @@ -104,7 +105,7 @@ } @media (prefers-color-scheme: dark) { - #sidebar #search img { + #sidebar #search svg { filter: brightness(0) invert(1); } } diff --git a/lib/graphql-docs/layouts/assets/images/search.svg b/lib/graphql-docs/layouts/assets/images/search.svg deleted file mode 100644 index eee8ff56..00000000 --- a/lib/graphql-docs/layouts/assets/images/search.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/lib/graphql-docs/layouts/includes/sidebar.html b/lib/graphql-docs/layouts/includes/sidebar.html index 6f7c2be6..0f480f54 100644 --- a/lib/graphql-docs/layouts/includes/sidebar.html +++ b/lib/graphql-docs/layouts/includes/sidebar.html @@ -1,5 +1,7 @@