elder_docs 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,215 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=Syne:wght@500;600;700&family=IBM+Plex+Sans:wght@400;500;600&family=Inter:wght@400;600;800&family=Space+Grotesk:wght@400;600;700&family=Oswald:wght@400;600;700&family=Fira+Code:wght@400;600&family=Roboto:wght@400;500;700&family=Open+Sans:wght@400;600;700&display=swap');
2
+
3
+ @tailwind base;
4
+ @tailwind components;
5
+ @tailwind utilities;
6
+
7
+ @layer base {
8
+ :root {
9
+ --bd-yellow: #f8d447;
10
+ --bd-charcoal: #000000;
11
+ --bd-ink: #121212;
12
+ --bd-white: #ffffff;
13
+ --bd-muted: #666666;
14
+ --bd-panel: #ffffff;
15
+ --bd-border: #000000;
16
+
17
+ --bd-radius: 0px;
18
+ --font-heading: 'Syne';
19
+ --font-body: 'IBM Plex Sans';
20
+ }
21
+
22
+ * {
23
+ border-width: 0;
24
+ }
25
+
26
+ body {
27
+ font-family: var(--font-body), system-ui, -apple-system, BlinkMacSystemFont;
28
+ font-size: 15px;
29
+ line-height: 1.55;
30
+ color: var(--bd-ink);
31
+ background: var(--bd-white);
32
+ min-height: 100vh;
33
+ }
34
+
35
+ body::before {
36
+ display: none;
37
+ }
38
+
39
+ body::after {
40
+ display: none;
41
+ }
42
+
43
+ body[data-mounted='true'] .reveal {
44
+ opacity: 1;
45
+ transform: translateY(0) scale(1);
46
+ }
47
+ }
48
+
49
+ @layer components {
50
+ .app-shell {
51
+ position: relative;
52
+ z-index: 1;
53
+ width: 100%;
54
+ height: 100%;
55
+ display: flex;
56
+ background: #ffffff;
57
+ }
58
+
59
+ .surface {
60
+ background: var(--bd-panel);
61
+ border: 3px solid var(--bd-border);
62
+ box-shadow: 8px 8px 0px 0px var(--bd-border);
63
+ border-radius: var(--bd-radius);
64
+ }
65
+
66
+ .surface--glass {
67
+ backdrop-filter: none;
68
+ background: var(--bd-panel);
69
+ }
70
+
71
+ .surface--highlight {
72
+ border-color: var(--bd-border);
73
+ box-shadow: 4px 4px 0px 0px var(--bd-yellow);
74
+ }
75
+
76
+ .pill {
77
+ font-family: var(--font-heading), system-ui;
78
+ letter-spacing: 0.1em;
79
+ font-size: 0.7rem;
80
+ text-transform: uppercase;
81
+ border: 2px solid var(--bd-border);
82
+ padding: 0.35rem 1rem;
83
+ border-radius: var(--bd-radius);
84
+ font-weight: 700;
85
+ color: var(--bd-charcoal);
86
+ background: var(--bd-yellow);
87
+ }
88
+
89
+ .chip {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ gap: 0.25rem;
93
+ font-size: 0.7rem;
94
+ text-transform: uppercase;
95
+ letter-spacing: 0.1em;
96
+ padding: 0.3rem 0.9rem;
97
+ border-radius: var(--bd-radius);
98
+ border: 2px solid var(--bd-border);
99
+ font-weight: 700;
100
+ background: var(--bd-panel);
101
+ color: var(--bd-ink);
102
+ }
103
+
104
+ .btn-primary {
105
+ font-family: var(--font-heading), system-ui;
106
+ text-transform: uppercase;
107
+ letter-spacing: 0.1em;
108
+ font-size: 0.8rem;
109
+ background: var(--bd-charcoal);
110
+ color: var(--bd-white);
111
+ padding: 0.85rem 1.6rem;
112
+ border: 3px solid var(--bd-border);
113
+ border-radius: var(--bd-radius);
114
+ transition: transform 0.1s ease, box-shadow 0.1s ease;
115
+ font-weight: 700;
116
+ box-shadow: 4px 4px 0px 0px var(--bd-yellow);
117
+ }
118
+
119
+ .btn-primary:hover {
120
+ transform: translate(-2px, -2px);
121
+ box-shadow: 6px 6px 0px 0px var(--bd-yellow);
122
+ }
123
+
124
+ .btn-primary:active {
125
+ transform: translate(2px, 2px);
126
+ box-shadow: 0px 0px 0px 0px var(--bd-yellow);
127
+ }
128
+
129
+ .btn-secondary {
130
+ font-family: var(--font-heading), system-ui;
131
+ text-transform: uppercase;
132
+ letter-spacing: 0.1em;
133
+ font-size: 0.7rem;
134
+ background: var(--bd-panel);
135
+ color: var(--bd-ink);
136
+ padding: 0.75rem 1.3rem;
137
+ border: 2px solid var(--bd-border);
138
+ border-radius: var(--bd-radius);
139
+ transition: transform 0.1s ease;
140
+ font-weight: 700;
141
+ }
142
+
143
+ .btn-secondary:hover {
144
+ background: var(--bd-yellow);
145
+ color: var(--bd-charcoal);
146
+ }
147
+
148
+ .input-field {
149
+ background: var(--bd-panel);
150
+ border: 3px solid var(--bd-border);
151
+ color: var(--bd-ink);
152
+ border-radius: var(--bd-radius);
153
+ padding: 0.65rem 0.9rem;
154
+ transition: box-shadow 0.1s ease;
155
+ font-weight: 500;
156
+ }
157
+
158
+ .input-field:focus {
159
+ outline: none;
160
+ box-shadow: 4px 4px 0px 0px var(--bd-yellow);
161
+ }
162
+
163
+ .nav-card {
164
+ border-radius: var(--bd-radius);
165
+ border: 2px solid transparent;
166
+ background: transparent;
167
+ transition: transform 0.1s ease;
168
+ }
169
+
170
+ .nav-card:hover {
171
+ background: var(--bd-panel);
172
+ border: 2px solid var(--bd-border);
173
+ transform: translate(-2px, -2px);
174
+ box-shadow: 4px 4px 0px 0px var(--bd-charcoal);
175
+ }
176
+
177
+ .nav-card--active {
178
+ background: var(--bd-yellow);
179
+ border: 2px solid var(--bd-border);
180
+ color: var(--bd-charcoal);
181
+ box-shadow: 4px 4px 0px 0px var(--bd-charcoal);
182
+ transform: translate(-2px, -2px);
183
+ }
184
+
185
+ .reveal {
186
+ opacity: 0;
187
+ transform: translateY(15px);
188
+ transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1), transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
189
+ }
190
+ }
191
+
192
+ @keyframes floatGlow {
193
+ 0% {
194
+ transform: translateY(0px);
195
+ opacity: 0.5;
196
+ }
197
+ 50% {
198
+ transform: translateY(-12px);
199
+ opacity: 1;
200
+ }
201
+ 100% {
202
+ transform: translateY(0px);
203
+ opacity: 0.5;
204
+ }
205
+ }
206
+
207
+ @keyframes shimmer {
208
+ 0% {
209
+ background-position: 0% 50%;
210
+ }
211
+ 100% {
212
+ background-position: 200% 50%;
213
+ }
214
+ }
215
+
@@ -0,0 +1,11 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
11
+
@@ -0,0 +1,40 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {
9
+ fontFamily: {
10
+ sans: ['Inter', 'system-ui', 'sans-serif'],
11
+ },
12
+ fontSize: {
13
+ 'xs': '16px',
14
+ 'sm': '18px',
15
+ 'base': '20px',
16
+ 'lg': '24px',
17
+ 'xl': '28px',
18
+ '2xl': '32px',
19
+ '3xl': '40px',
20
+ '4xl': '48px',
21
+ },
22
+ colors: {
23
+ 'yellow': {
24
+ 50: '#FFFBEB',
25
+ 100: '#FEF3C7',
26
+ 200: '#FDE68A',
27
+ 300: '#FCD34D',
28
+ 400: '#FBBF24',
29
+ 500: '#F59E0B',
30
+ 600: '#D97706',
31
+ }
32
+ },
33
+ borderRadius: {
34
+ 'none': '0',
35
+ }
36
+ },
37
+ },
38
+ plugins: [],
39
+ }
40
+
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ build: {
7
+ outDir: 'dist',
8
+ assetsDir: 'assets',
9
+ rollupOptions: {
10
+ output: {
11
+ manualChunks: undefined
12
+ }
13
+ }
14
+ },
15
+ base: './'
16
+ })
17
+
@@ -8,7 +8,7 @@ module ElderDocs
8
8
  desc 'deploy', 'Generate and deploy API documentation'
9
9
  method_option :definitions, type: :string, default: 'definitions.json', aliases: '-d'
10
10
  method_option :articles, type: :string, default: 'articles.json', aliases: '-a'
11
- method_option :output, type: :string, default: nil, aliases: '-o'
11
+ method_option :output, type: :string, default: nil, aliases: '-o', desc: 'Directory to write built assets (default: public/elderdocs)'
12
12
  method_option :api_server, type: :string, default: nil, desc: 'Default API server URL'
13
13
  method_option :skip_build, type: :boolean, default: false, desc: 'Skip frontend build if assets exist'
14
14
  method_option :force_build, type: :boolean, default: false, desc: 'Force rebuilding frontend assets'
@@ -29,10 +29,14 @@ module ElderDocs
29
29
  File.write(articles_path, [].to_json)
30
30
  end
31
31
 
32
+ output_path = File.expand_path(options[:output] || default_output_path, Dir.pwd)
33
+
34
+ ElderDocs.config.output_path = output_path
35
+
32
36
  generator = Generator.new(
33
37
  definitions_path: definitions_path,
34
38
  articles_path: articles_path,
35
- output_path: options[:output] || default_output_path,
39
+ output_path: output_path,
36
40
  api_server: options[:api_server],
37
41
  skip_build: options[:skip_build],
38
42
  force_build: options[:force_build]
@@ -62,7 +66,7 @@ module ElderDocs
62
66
  private
63
67
 
64
68
  def default_output_path
65
- File.join(File.dirname(__FILE__), '..', '..', 'lib', 'elder_docs', 'assets', 'viewer')
69
+ ElderDocs.config.output_path || File.join(Dir.pwd, 'public', 'elderdocs')
66
70
  end
67
71
  end
68
72
  end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'yaml'
4
+ require 'pathname'
4
5
 
5
6
  module ElderDocs
6
7
  class Config
7
- attr_accessor :mount_path, :api_server, :auth_types, :ui_config, :admin_password
8
+ attr_accessor :mount_path, :api_server, :auth_types, :ui_config, :admin_password, :output_path
8
9
 
9
10
  def initialize
10
11
  @mount_path = nil
@@ -12,6 +13,8 @@ module ElderDocs
12
13
  @auth_types = ['bearer', 'api_key', 'basic', 'oauth2']
13
14
  @ui_config = {}
14
15
  @admin_password = nil
16
+ @api_servers = []
17
+ @output_path = default_output_path
15
18
  load_config_file
16
19
  end
17
20
 
@@ -29,19 +32,34 @@ module ElderDocs
29
32
  return unless config_path
30
33
 
31
34
  begin
32
- config = YAML.load_file(config_path)
33
- @mount_path = config['mount_path'] if config['mount_path']
34
- @api_server = config['api_server'] if config['api_server']
35
- @api_servers = config['api_servers'] if config['api_servers']
36
- @auth_types = config['auth_types'] if config['auth_types']
37
- @ui_config = config['ui'] if config['ui'] # YAML uses 'ui' key, but we store as ui_config
38
- @admin_password = config['admin_password'] if config['admin_password']
39
- rescue => e
40
- warn "Warning: Could not load elderdocs.yml: #{e.message}"
35
+ config = YAML.load_file(config_path)
36
+ config_dir = File.dirname(config_path)
37
+ @mount_path = config['mount_path'] if config['mount_path']
38
+ @api_server = config['api_server'] if config['api_server']
39
+ @api_servers = config['api_servers'] if config['api_servers']
40
+ @auth_types = config['auth_types'] if config['auth_types']
41
+ @ui_config = config['ui'] if config['ui'] # YAML uses 'ui' key, but we store as ui_config
42
+ @admin_password = config['admin_password'] if config['admin_password']
43
+ if config['output_path']
44
+ @output_path = File.expand_path(config['output_path'], config_dir)
41
45
  end
46
+ rescue => e
47
+ warn "Warning: Could not load elderdocs.yml: #{e.message}"
42
48
  end
49
+ end
50
+
51
+ def default_output_path
52
+ base_path =
53
+ if defined?(Rails) && Rails.root
54
+ Rails.root
55
+ else
56
+ Pathname.new(Dir.pwd)
57
+ end
43
58
 
44
- attr_reader :api_servers
59
+ base_path.join('public', 'elderdocs').to_s
60
+ end
61
+
62
+ attr_reader :api_servers
45
63
 
46
64
  def self.instance
47
65
  @instance ||= new
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pathname'
4
+
3
5
  module ElderDocs
4
6
  class Engine < ::Rails::Engine
5
7
  isolate_namespace ElderDocs
@@ -47,7 +49,7 @@ module ElderDocs
47
49
  include ActionController::MimeResponds
48
50
 
49
51
  def show
50
- viewer_path = Engine.root.join('lib', 'elder_docs', 'assets', 'viewer')
52
+ viewer_path = resolve_viewer_path
51
53
  requested_path = params[:path]
52
54
  requested_path = requested_path.present? ? requested_path : 'index'
53
55
  requested_path = [requested_path, params[:format]].compact.join('.')
@@ -69,6 +71,19 @@ module ElderDocs
69
71
 
70
72
  private
71
73
 
74
+ def resolve_viewer_path
75
+ custom_path = ElderDocs.config.output_path
76
+ if custom_path && Dir.exist?(custom_path)
77
+ Pathname.new(custom_path)
78
+ else
79
+ fallback_viewer_path
80
+ end
81
+ end
82
+
83
+ def fallback_viewer_path
84
+ Engine.root.join('lib', 'elder_docs', 'assets', 'viewer')
85
+ end
86
+
72
87
  def mime_type_for(file_path)
73
88
  ext = File.extname(file_path.to_s)
74
89
  case ext
@@ -13,7 +13,7 @@ module ElderDocs
13
13
  def initialize(definitions_path:, articles_path:, output_path:, api_server: nil, skip_build: false, force_build: false)
14
14
  @definitions_path = definitions_path
15
15
  @articles_path = articles_path
16
- @output_path = output_path
16
+ @output_path = File.expand_path(output_path, Dir.pwd)
17
17
  @api_server = api_server
18
18
  @skip_build = skip_build
19
19
  @force_build = force_build
@@ -185,12 +185,28 @@ module ElderDocs
185
185
  def build_frontend!
186
186
  say '🔨 Building frontend...', :cyan
187
187
 
188
- frontend_dir = File.join(File.dirname(__FILE__), '..', '..', 'frontend')
188
+ # Try multiple locations for frontend directory
189
+ # 1. Relative to lib (development repo)
190
+ # 2. At gem root (installed gem)
191
+ # 3. Using Engine.root if available (Rails context)
192
+ possible_paths = [
193
+ File.join(File.dirname(__FILE__), '..', '..', 'frontend'), # Development: lib/elder_docs/../../frontend
194
+ File.join(File.dirname(__FILE__), '..', '..', '..', 'frontend'), # Installed gem: lib/elder_docs/../../../frontend
195
+ ]
189
196
 
190
- unless Dir.exist?(frontend_dir)
191
- raise ValidationError, "Frontend directory not found at #{frontend_dir}"
197
+ # If Engine is available, try relative to gem root
198
+ if defined?(ElderDocs::Engine) && ElderDocs::Engine.root
199
+ possible_paths << ElderDocs::Engine.root.join('frontend').to_s
192
200
  end
193
201
 
202
+ frontend_dir = possible_paths.find { |path| Dir.exist?(path) }
203
+
204
+ unless frontend_dir && Dir.exist?(frontend_dir)
205
+ raise ValidationError, "Frontend directory not found. Tried: #{possible_paths.join(', ')}"
206
+ end
207
+
208
+ frontend_dir = File.expand_path(frontend_dir)
209
+
194
210
  # Write compiled data to frontend public directory
195
211
  public_dir = File.join(frontend_dir, 'public')
196
212
  FileUtils.mkdir_p(public_dir)
@@ -240,13 +256,12 @@ module ElderDocs
240
256
  index_html_path = File.join(output_path, 'index.html')
241
257
  if File.exist?(index_html_path)
242
258
  html_content = File.read(index_html_path, encoding: 'UTF-8')
243
- # Replace relative paths with paths relative to mount point
244
- html_content.gsub!(/src="\.\//, 'src="/docs/')
245
- html_content.gsub!(/href="\.\//, 'href="/docs/')
246
- html_content.gsub!(/src="\/assets\//, 'src="/docs/assets/')
247
- html_content.gsub!(/href="\/assets\//, 'href="/docs/assets/')
248
- # Fix data.js path
249
- html_content.gsub!(/src="\/data\.js/, 'src="/docs/data.js')
259
+ mount_path = normalized_mount_path
260
+ html_content.gsub!(/src="\.\//, %{src="#{mount_path}/})
261
+ html_content.gsub!(/href="\.\//, %{href="#{mount_path}/})
262
+ html_content.gsub!(/src="\/assets\//, %{src="#{mount_path}/assets/})
263
+ html_content.gsub!(/href="\/assets\//, %{href="#{mount_path}/assets/})
264
+ html_content.gsub!(/src="\/data\.js/, %{src="#{mount_path}/data.js})
250
265
  File.write(index_html_path, html_content)
251
266
  end
252
267
 
@@ -258,6 +273,14 @@ module ElderDocs
258
273
 
259
274
  Thor::Base.shell.new.say(message, color)
260
275
  end
276
+
277
+ def normalized_mount_path
278
+ mount_path = ElderDocs.config.mount_path || '/docs'
279
+ mount_path = "/#{mount_path}" unless mount_path.start_with?('/')
280
+ mount_path = '/' if mount_path == '//'
281
+ mount_path = mount_path.chomp('/')
282
+ mount_path.empty? ? '' : mount_path
283
+ end
261
284
  end
262
285
  end
263
286
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ElderDocs
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.3'
5
5
  end
6
6
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elder_docs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - ElderDocs
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.0'
33
+ version: '2.0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.0'
40
+ version: '2.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rails
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -107,6 +107,21 @@ files:
107
107
  - README.md
108
108
  - elderdocs.yml.example
109
109
  - exe/elderdocs
110
+ - frontend/index.html
111
+ - frontend/package.json
112
+ - frontend/postcss.config.js
113
+ - frontend/public/data.js
114
+ - frontend/src/App.jsx
115
+ - frontend/src/UiConfigApp.jsx
116
+ - frontend/src/components/ApiExplorer.jsx
117
+ - frontend/src/components/ContentPanel.jsx
118
+ - frontend/src/components/Sidebar.jsx
119
+ - frontend/src/components/UiConfigurator.jsx
120
+ - frontend/src/contexts/ApiKeyContext.jsx
121
+ - frontend/src/index.css
122
+ - frontend/src/main.jsx
123
+ - frontend/tailwind.config.js
124
+ - frontend/vite.config.js
110
125
  - lib/elder_docs.rb
111
126
  - lib/elder_docs/assets/ui_config.html
112
127
  - lib/elder_docs/assets/viewer/assets/index-161b367b.css