Namespacing
Components go under one module of your choice. Pick a name that won't clash with your own code.
Every component is written once, against UI. The generators rewrite each file as they copy it, so a namespace changes more than the class names:
| In VelvetKit | In your app, with Kit |
|---|---|
UI::Button |
Kit::Button |
app/components/ui/ |
app/components/kit/ |
data-controller="ui--menu" |
data-controller="kit--menu" |
t("ui.dialog.close_label") |
t("kit.dialog.close_label") |
"ui-dialog-#{id}" |
"kit-dialog-#{id}" |
Try it
LiveType a module name. These are real component files, rewritten by the same code the generator runs.
Folder: kit/
Stimulus: kit--
Translations: kit.
app/components/kit/dialog.rb
class Kit::Dialog < Kit::Base
renders_one :trigger, ->(**opts) {
Kit::Dialog::Trigger.new(**opts).tap { |t| t.set_dialog_context(self) }
}
renders_one :body, ->(**opts) {
Kit::Dialog::Content.new(**opts).tap { |c| c.set_dialog_context(self) }
}
alias_method :with_content, :with_body
def initialize(class: nil, **html_attrs)
@user_class = binding.local_variable_get(:class)
@html_attrs = html_attrs
@id = @html_attrs[:id] || "kit-dialog-#{SecureRandom.hex(4)}"
@html_attrs.delete(:id)
end
attr_reader :id
app/components/kit/sheet.html.erb
<%= tag.div(class: root_classes,
data: { controller: "kit--dialog" }.merge(@html_attrs[:data] || {}),
**@html_attrs.except(:data)) do %><% if trigger? %><%= trigger %><% end %><% if body? %><%= body %><% end %><% end %>
app/javascript/controllers/kit/theme_controller.ts
export default class extends Controller<HTMLElement> {
static targets = ['option'];
declare readonly hasOptionTarget: boolean;
declare readonly optionTargets: HTMLElement[];
private media?: MediaQueryList;
private onSystemChange?: () => void;
connect() {
this.media = window.matchMedia('(prefers-color-scheme: dark)');
// An OS-level switch should move the page live, but only while the user
// has not pinned an explicit preference.
this.onSystemChange = () => {
if (this.preference() === 'system') this.apply();
};
this.media.addEventListener('change', this.onSystemChange);
this.apply();
All-capital names
Rails reads app/components/ui as Ui, not UI. For names like that, install adds an inflection so Zeitwerk finds your classes:
config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym "UI"
end
Choose once
To change the namespace later, rename the folder and every UI:: reference yourself, or delete the components and add them again under the new name.