its-swiss 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +57 -0
  3. data/LICENSE +21 -0
  4. data/README.md +262 -0
  5. data/app/assets/javascripts/its_swiss/clipboard_controller.js +33 -0
  6. data/app/assets/stylesheets/its-swiss.css +16 -0
  7. data/app/assets/stylesheets/its_swiss/components.css +403 -0
  8. data/app/assets/stylesheets/its_swiss/grid.css +67 -0
  9. data/app/assets/stylesheets/its_swiss/reset.css +50 -0
  10. data/app/assets/stylesheets/its_swiss/specimen.css +37 -0
  11. data/app/assets/stylesheets/its_swiss/tokens.css +142 -0
  12. data/app/assets/stylesheets/its_swiss/transitions.css +47 -0
  13. data/app/assets/stylesheets/its_swiss/type.css +109 -0
  14. data/app/controllers/its_swiss/specimen_controller.rb +24 -0
  15. data/app/helpers/its_swiss/application_helper.rb +73 -0
  16. data/app/views/its_swiss/shared/_errors.html.erb +14 -0
  17. data/app/views/its_swiss/shared/_flash.html.erb +8 -0
  18. data/app/views/its_swiss/shared/_masthead.html.erb +15 -0
  19. data/app/views/its_swiss/shared/_pagination.html.erb +26 -0
  20. data/app/views/its_swiss/specimen/_buttons.html.erb +15 -0
  21. data/app/views/its_swiss/specimen/_footer.html.erb +7 -0
  22. data/app/views/its_swiss/specimen/_form.html.erb +18 -0
  23. data/app/views/its_swiss/specimen/_grid.html.erb +14 -0
  24. data/app/views/its_swiss/specimen/_masthead.html.erb +15 -0
  25. data/app/views/its_swiss/specimen/_messages.html.erb +13 -0
  26. data/app/views/its_swiss/specimen/_pagination.html.erb +4 -0
  27. data/app/views/its_swiss/specimen/_pairs.html.erb +8 -0
  28. data/app/views/its_swiss/specimen/_table.html.erb +15 -0
  29. data/app/views/its_swiss/specimen/_take.html.erb +19 -0
  30. data/app/views/its_swiss/specimen/_type.html.erb +24 -0
  31. data/app/views/its_swiss/specimen/_values.html.erb +18 -0
  32. data/app/views/its_swiss/specimen/show.html.erb +27 -0
  33. data/app/views/layouts/its_swiss/shell.html.erb +55 -0
  34. data/config/importmap.rb +5 -0
  35. data/config/routes.rb +6 -0
  36. data/lib/generators/its_swiss/install/install_generator.rb +70 -0
  37. data/lib/generators/its_swiss/install/templates/theme.css +44 -0
  38. data/lib/its-swiss.rb +3 -0
  39. data/lib/its_swiss/engine.rb +39 -0
  40. data/lib/its_swiss/form_builder.rb +140 -0
  41. data/lib/its_swiss/version.rb +3 -0
  42. data/lib/its_swiss.rb +46 -0
  43. metadata +172 -0
@@ -0,0 +1,70 @@
1
+ require "rails/generators/base"
2
+
3
+ module ItsSwiss
4
+ module Generators
5
+ # Four things, each of them easy to get wrong by hand and quiet when it is
6
+ # wrong. Everything else the library does is a gem dependency and needs no
7
+ # generator at all: Propshaft finds the stylesheets, and the engine pins
8
+ # its own JavaScript so that upgrading the gem is enough to get the new
9
+ # file.
10
+ class InstallGenerator < Rails::Generators::Base
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ desc "Fills in the slots its-swiss leaves for an application: the accent, the typeface, the grid."
14
+
15
+ def create_theme_stylesheet
16
+ template "theme.css", "app/assets/stylesheets/theme.css"
17
+ end
18
+
19
+ # An application that has already chosen a layout has chosen it.
20
+ def render_through_the_shell
21
+ controller = "app/controllers/application_controller.rb"
22
+ return say_status(:skip, "#{controller} not found", :yellow) unless exists?(controller)
23
+
24
+ contents = read(controller)
25
+ return say_status(:skip, "#{controller} already chooses a layout", :yellow) if contents.match?(/^\s*layout\s/)
26
+
27
+ inject_into_class controller, "ApplicationController", %( layout "its_swiss/shell"\n)
28
+ end
29
+
30
+ # Development only, and the controller refuses it a second time: the
31
+ # specimen names every component the library has, and a route is a line
32
+ # in a file someone can move.
33
+ def mount_the_specimen
34
+ routes = "config/routes.rb"
35
+ return say_status(:skip, "#{routes} not found", :yellow) unless exists?(routes)
36
+ return say_status(:skip, "the specimen is already mounted", :yellow) if read(routes).include?("ItsSwiss::Engine")
37
+
38
+ inject_into_file routes, after: /Rails\.application\.routes\.draw do\n/ do
39
+ <<~RUBY.indent(2)
40
+ if Rails.env.development?
41
+ mount ItsSwiss::Engine => "/its-swiss"
42
+ end
43
+
44
+ RUBY
45
+ end
46
+ end
47
+
48
+ def say_what_is_left
49
+ say <<~TEXT
50
+
51
+ its-swiss is installed. What is left is yours:
52
+
53
+ app/assets/stylesheets/theme.css the accent, the typeface, the grid
54
+ /its-swiss/specimen every component, rendered twice
55
+
56
+ The library ships no typeface and no palette. Set --accent to one
57
+ colour and read the specimen with it unset: if the page still works,
58
+ the value scale is doing the work.
59
+ TEXT
60
+ end
61
+ private
62
+ # Thor writes relative to destination_root; reading has to as well, or
63
+ # the guards above interrogate whatever directory the generator was
64
+ # invoked from.
65
+ def exists?(path) = File.exist?(File.expand_path(path, destination_root))
66
+
67
+ def read(path) = File.read(File.expand_path(path, destination_root))
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,44 @@
1
+ /* What this application fills in.
2
+ *
3
+ * Unlayered on purpose. Everything its-swiss ships is inside a cascade layer,
4
+ * and an unlayered rule beats every layered one whatever its specificity — so
5
+ * this file wins without having to out-specify the library or reach for
6
+ * !important, and the grid and the domain components below it win too.
7
+ */
8
+
9
+ :root {
10
+ /* --- The accent --------------------------------------------------------
11
+ State and emphasis only: the current destination, focus, a link, the one
12
+ primary action, the moment before something cannot be undone. Never a
13
+ filled area for its own sake.
14
+
15
+ Delete this line and read the app. If it still works, the value scale is
16
+ doing the work, which is the point. */
17
+ --accent: oklch(57.7% 0.234 28);
18
+ --accent-ink: var(--paper);
19
+
20
+ /* --- The typeface ------------------------------------------------------
21
+ The library ships none. Declare an @font-face below and name it here, or
22
+ leave this out and take the grotesque stack the library falls back to. */
23
+ /* --font-family: "Your Face", ui-sans-serif, system-ui, sans-serif; */
24
+
25
+ /* --- The value scale ---------------------------------------------------
26
+ Neutral as it ships. These two turn the whole ladder together: a small
27
+ chroma at a yellow-green hue gives the warm grays of a printed page. */
28
+ /* --value-chroma: 0.006; */
29
+ /* --value-hue: 95; */
30
+
31
+ /* --- The grid ----------------------------------------------------------
32
+ How many fields this problem has. Six divides into halves and thirds and
33
+ is where the library starts; change it here once the pages have said what
34
+ they need, and every width in the app re-proportions. */
35
+ --columns: 6;
36
+ --gutter: 1.5rem;
37
+ --baseline: 0.5rem;
38
+ }
39
+
40
+ /* The grid itself lives here, not in the gem: which blocks span which fields
41
+ is what this application looks like.
42
+
43
+ main.grid > .page-head { --span: 4; }
44
+ */
data/lib/its-swiss.rb ADDED
@@ -0,0 +1,3 @@
1
+ # The gem is named with a hyphen and the library with an underscore, so
2
+ # `gem "its-swiss"` finds something to require.
3
+ require "its_swiss"
@@ -0,0 +1,39 @@
1
+ require "rails/engine"
2
+
3
+ module ItsSwiss
4
+ # Isolated, so the specimen's routes and helpers cannot collide with the
5
+ # application's. The stylesheets are not isolated by anything — CSS has one
6
+ # global namespace and pretending otherwise would mean prefixing every
7
+ # class in the library.
8
+ class Engine < ::Rails::Engine
9
+ isolate_namespace ItsSwiss
10
+
11
+ # Propshaft picks up an engine's app/assets/stylesheets on its own. The
12
+ # JavaScript is not in that default set, and it is one file, so it is
13
+ # added here rather than left to the application to remember.
14
+ initializer "its_swiss.assets" do |app|
15
+ next unless app.config.respond_to?(:assets)
16
+
17
+ app.config.assets.paths << root.join("app/assets/javascripts")
18
+ end
19
+
20
+ # Pinned from the engine rather than written into the application's
21
+ # importmap by the generator: an application that upgrades the gem should
22
+ # get the new file without re-running a generator, and a pin it never
23
+ # wrote is a pin it cannot leave stale.
24
+ initializer "its_swiss.importmap", before: "importmap" do |app|
25
+ next unless app.respond_to?(:importmap)
26
+
27
+ app.config.importmap.paths << root.join("config/importmap.rb")
28
+ # Reloading in development watches the pinned files for changes; a gem
29
+ # loaded from a path: dependency is being edited, so it is watched too.
30
+ app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
31
+ end
32
+
33
+ config.to_prepare do
34
+ ActiveSupport.on_load(:action_view) do
35
+ include ItsSwiss::ApplicationHelper
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,140 @@
1
+ module ItsSwiss
2
+ # One shape for every field: a label, a control, and — when there is
3
+ # something to say — a hint or the reason it was refused.
4
+ #
5
+ # Written by a builder rather than by hand because the parts that get left
6
+ # out by hand are the parts nobody sees missing. A label whose `for` does not
7
+ # match its input is a label that does not enlarge the target. A hint beside
8
+ # a control is a hint a screen reader never reaches. A refused field coloured
9
+ # by CSS alone is a refusal that only some readers get.
10
+ class FormBuilder < ActionView::Helpers::FormBuilder
11
+ # Everything that is a line of text under a rule. They differ only in the
12
+ # keyboard a phone puts up, which is not a difference the markup has.
13
+ TEXT_FIELDS = %i[
14
+ text_field email_field password_field number_field url_field
15
+ telephone_field phone_field search_field date_field time_field
16
+ datetime_field color_field text_area
17
+ ].freeze
18
+
19
+ TEXT_FIELDS.each do |control|
20
+ define_method(control) do |method, options = {}|
21
+ field(method, options) { |opts| super(method, opts) }
22
+ end
23
+ end
24
+
25
+ def select(method, choices = nil, options = {}, html_options = {}, &block)
26
+ field(method, html_options) do |opts|
27
+ super(method, choices, options, opts, &block)
28
+ end
29
+ end
30
+
31
+ def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
32
+ field(method, html_options) do |opts|
33
+ super(method, collection, value_method, text_method, options, opts)
34
+ end
35
+ end
36
+
37
+ # A checkbox and its label are a pair on one line — the label says what
38
+ # ticking it means, and reading that above an empty box says nothing.
39
+ def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
40
+ field(method, options, label_beside: true) do |opts|
41
+ super(method, opts, checked_value, unchecked_value)
42
+ end
43
+ end
44
+
45
+ def radio_button(method, tag_value, options = {})
46
+ field(method, options, label_beside: true, label_for_value: tag_value) do |opts|
47
+ super(method, tag_value, opts)
48
+ end
49
+ end
50
+
51
+ # A <button>, not an <input>: an input's label is an attribute, and the
52
+ # one thing this style asks of a button is that it be a box with words in
53
+ # it. Primary unless the caller says otherwise, because a form has one
54
+ # thing it is for.
55
+ def submit(value = nil, options = {})
56
+ value ||= submit_default_value
57
+ options = { class: "button button--primary" }.merge(options.symbolize_keys)
58
+
59
+ @template.tag.button(value, **options, type: "submit")
60
+ end
61
+
62
+ # The wrapper on its own, for a control the library does not know about.
63
+ def field(method, options = {}, label_beside: false, label_for_value: nil, &control)
64
+ options = options.symbolize_keys
65
+ hint, label_option = options.delete(:hint), options.delete(:label)
66
+ errors = errors_on(method)
67
+
68
+ ids = describers(method, hint, errors)
69
+ options[:"aria-describedby"] = ids.join(" ") if ids.any?
70
+ options[:"aria-invalid"] = "true" if errors.any?
71
+
72
+ label = rendered_label(method, label_option, label_for_value)
73
+ options[:"aria-label"] ||= default_label(method) if label.nil?
74
+ body = @template.capture { control.call(options) }
75
+ body = @template.tag.span(safe_join([ body, label ]), class: "choice") if label_beside && label
76
+
77
+ @template.tag.div(class: field_classes(errors)) do
78
+ safe_join([
79
+ (label unless label_beside),
80
+ body,
81
+ hint_tag(method, hint),
82
+ *errors.map.with_index { |message, index| error_tag(method, index, message) }
83
+ ].compact)
84
+ end
85
+ end
86
+
87
+ private
88
+ def field_classes(errors)
89
+ errors.any? ? "field field--invalid" : "field"
90
+ end
91
+
92
+ # `label: false` hides the label; it does not remove it. Something has to
93
+ # name the control, so the name moves onto the control itself.
94
+ def rendered_label(method, option, for_value)
95
+ return nil if option == false
96
+
97
+ label(method, option.is_a?(String) ? option : nil, value: for_value)
98
+ end
99
+
100
+ # A control whose label is hidden still needs a name, and aria-label is
101
+ # where it goes. Applied to the control rather than the wrapper because
102
+ # the wrapper is a div and names nothing.
103
+ def describers(method, hint, errors)
104
+ ids = []
105
+ ids << hint_id(method) if hint
106
+ errors.each_index { |index| ids << error_id(method, index) }
107
+ ids
108
+ end
109
+
110
+ def hint_tag(method, hint)
111
+ return nil unless hint
112
+
113
+ @template.tag.p(hint, class: "hint", id: hint_id(method))
114
+ end
115
+
116
+ def error_tag(method, index, message)
117
+ @template.tag.p(message, class: "field__error", id: error_id(method, index))
118
+ end
119
+
120
+ # What the label would have said, for a control whose label is hidden.
121
+ def default_label(method)
122
+ @object_name.to_s.camelize.safe_constantize&.human_attribute_name(method) ||
123
+ method.to_s.humanize
124
+ end
125
+
126
+ def hint_id(method) = "#{field_id(method)}_hint"
127
+
128
+ def error_id(method, index) = "#{field_id(method)}_error_#{index}"
129
+
130
+ # Full messages, so "cannot be blank" reads as a sentence about the
131
+ # field rather than as a fragment floating under it.
132
+ def errors_on(method)
133
+ return [] unless @object.respond_to?(:errors)
134
+
135
+ Array(@object.errors.full_messages_for(method))
136
+ end
137
+
138
+ def safe_join(parts) = @template.safe_join(parts)
139
+ end
140
+ end
@@ -0,0 +1,3 @@
1
+ module ItsSwiss
2
+ VERSION = "0.1.0"
3
+ end
data/lib/its_swiss.rb ADDED
@@ -0,0 +1,46 @@
1
+ require "its_swiss/version"
2
+ require "its_swiss/engine" if defined?(Rails::Engine)
3
+
4
+ module ItsSwiss
5
+ # Autoloaded rather than required: it subclasses ActionView's builder, which
6
+ # does not exist until Action View has loaded, and this file is read while
7
+ # the application is still assembling itself.
8
+ autoload :FormBuilder, "its_swiss/form_builder"
9
+
10
+ # The stylesheets, in the order their layers are declared. The gem's own
11
+ # layer order is fixed here rather than left to whoever writes the <link>
12
+ # tags: an application that loads type before tokens should still get the
13
+ # cascade the library was designed with.
14
+ #
15
+ # Every file states its own layer, so linking these six individually and
16
+ # linking the single its-swiss.css that imports them resolve identically.
17
+ STYLESHEETS = %w[ tokens reset type grid components transitions ].freeze
18
+
19
+ # The cascade layer everything the gem ships lives in. An application's own
20
+ # CSS is unlayered, and unlayered rules beat every layered one regardless of
21
+ # specificity — which is the boundary in the table made mechanical: the gem
22
+ # is a base, and the app's grid and domain components win without anyone
23
+ # having to count selectors or reach for !important.
24
+ LAYER = "its-swiss".freeze
25
+
26
+ class << self
27
+ # Where a consumer says which of the gem's slots it fills. Nothing here
28
+ # has a default the gem could reasonably pick for it.
29
+ def configure
30
+ yield config
31
+ end
32
+
33
+ def config
34
+ @config ||= Configuration.new
35
+ end
36
+ end
37
+
38
+ class Configuration
39
+ # The specimen is documentation and a regression fixture, not a page an
40
+ # application serves. It stays off unless the environment asks for it, so
41
+ # a route left mounted by accident cannot answer in production.
42
+ attr_writer :specimen
43
+
44
+ def specimen? = @specimen.nil? ? !!Rails.env.local? : @specimen
45
+ end
46
+ end
metadata ADDED
@@ -0,0 +1,172 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: its-swiss
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Bobby Meyer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: railties
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '8.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '10'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '8.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '10'
33
+ - !ruby/object:Gem::Dependency
34
+ name: actionview
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '8.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '10'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '8.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '10'
53
+ - !ruby/object:Gem::Dependency
54
+ name: propshaft
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '1.0'
60
+ - - "<"
61
+ - !ruby/object:Gem::Version
62
+ version: '3'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '1.0'
70
+ - - "<"
71
+ - !ruby/object:Gem::Version
72
+ version: '3'
73
+ - !ruby/object:Gem::Dependency
74
+ name: importmap-rails
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '2.0'
80
+ - - "<"
81
+ - !ruby/object:Gem::Version
82
+ version: '4'
83
+ type: :runtime
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '2.0'
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: '4'
93
+ description: |-
94
+ Tokens, reset, typography, grid primitives and components in the spirit of
95
+ the International Typographic Style, as plain CSS plus a Rails engine that
96
+ ships it. Monochrome by default: the value scale does the work and the one
97
+ accent is the consuming application's to set.
98
+ email:
99
+ - bobby@bobbymeyer.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - CHANGELOG.md
105
+ - LICENSE
106
+ - README.md
107
+ - app/assets/javascripts/its_swiss/clipboard_controller.js
108
+ - app/assets/stylesheets/its-swiss.css
109
+ - app/assets/stylesheets/its_swiss/components.css
110
+ - app/assets/stylesheets/its_swiss/grid.css
111
+ - app/assets/stylesheets/its_swiss/reset.css
112
+ - app/assets/stylesheets/its_swiss/specimen.css
113
+ - app/assets/stylesheets/its_swiss/tokens.css
114
+ - app/assets/stylesheets/its_swiss/transitions.css
115
+ - app/assets/stylesheets/its_swiss/type.css
116
+ - app/controllers/its_swiss/specimen_controller.rb
117
+ - app/helpers/its_swiss/application_helper.rb
118
+ - app/views/its_swiss/shared/_errors.html.erb
119
+ - app/views/its_swiss/shared/_flash.html.erb
120
+ - app/views/its_swiss/shared/_masthead.html.erb
121
+ - app/views/its_swiss/shared/_pagination.html.erb
122
+ - app/views/its_swiss/specimen/_buttons.html.erb
123
+ - app/views/its_swiss/specimen/_footer.html.erb
124
+ - app/views/its_swiss/specimen/_form.html.erb
125
+ - app/views/its_swiss/specimen/_grid.html.erb
126
+ - app/views/its_swiss/specimen/_masthead.html.erb
127
+ - app/views/its_swiss/specimen/_messages.html.erb
128
+ - app/views/its_swiss/specimen/_pagination.html.erb
129
+ - app/views/its_swiss/specimen/_pairs.html.erb
130
+ - app/views/its_swiss/specimen/_table.html.erb
131
+ - app/views/its_swiss/specimen/_take.html.erb
132
+ - app/views/its_swiss/specimen/_type.html.erb
133
+ - app/views/its_swiss/specimen/_values.html.erb
134
+ - app/views/its_swiss/specimen/show.html.erb
135
+ - app/views/layouts/its_swiss/shell.html.erb
136
+ - config/importmap.rb
137
+ - config/routes.rb
138
+ - lib/generators/its_swiss/install/install_generator.rb
139
+ - lib/generators/its_swiss/install/templates/theme.css
140
+ - lib/its-swiss.rb
141
+ - lib/its_swiss.rb
142
+ - lib/its_swiss/engine.rb
143
+ - lib/its_swiss/form_builder.rb
144
+ - lib/its_swiss/version.rb
145
+ homepage: https://github.com/bobbymeyer/its-swiss
146
+ licenses:
147
+ - MIT
148
+ metadata:
149
+ source_code_uri: https://github.com/bobbymeyer/its-swiss
150
+ bug_tracker_uri: https://github.com/bobbymeyer/its-swiss/issues
151
+ changelog_uri: https://github.com/bobbymeyer/its-swiss/blob/main/CHANGELOG.md
152
+ rubygems_mfa_required: 'true'
153
+ post_install_message:
154
+ rdoc_options: []
155
+ require_paths:
156
+ - lib
157
+ required_ruby_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: 3.2.0
162
+ required_rubygems_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ requirements: []
168
+ rubygems_version: 3.5.22
169
+ signing_key:
170
+ specification_version: 4
171
+ summary: A Swiss typographic style for Rails applications.
172
+ test_files: []