{"id":13877898,"url":"https://github.com/inhouse-work/protos","last_synced_at":"2025-10-27T08:09:06.354Z","repository":{"id":225388244,"uuid":"765864621","full_name":"inhouse-work/protos","owner":"inhouse-work","description":"A UI component library built with Phlex, Tailwindcss, and daisyUI","archived":false,"fork":false,"pushed_at":"2025-10-01T23:47:00.000Z","size":372,"stargazers_count":85,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-10-08T02:58:29.737Z","etag":null,"topics":["daisyui","phlex","rails","rails-frontend","ruby","tailwindcss"],"latest_commit_sha":null,"homepage":"https://protos.inhouse.work/","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/inhouse-work.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-03-01T19:17:11.000Z","updated_at":"2025-10-06T00:19:04.000Z","dependencies_parsed_at":"2024-12-30T04:13:48.206Z","dependency_job_id":"2772300a-293d-4ec5-a725-b70e3b9342b6","html_url":"https://github.com/inhouse-work/protos","commit_stats":{"total_commits":148,"total_committers":1,"mean_commits":148.0,"dds":0.0,"last_synced_commit":"9a8586b5dd1d2597ee25219877b512a6e216d3af"},"previous_names":["inhouse-work/protos"],"tags_count":16,"template":false,"template_full_name":null,"purl":"pkg:github/inhouse-work/protos","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inhouse-work%2Fprotos","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inhouse-work%2Fprotos/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inhouse-work%2Fprotos/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inhouse-work%2Fprotos/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/inhouse-work","download_url":"https://codeload.github.com/inhouse-work/protos/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inhouse-work%2Fprotos/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":281235253,"owners_count":26466164,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-27T02:00:05.855Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["daisyui","phlex","rails","rails-frontend","ruby","tailwindcss"],"created_at":"2024-08-06T08:01:34.403Z","updated_at":"2025-10-27T08:09:06.346Z","avatar_url":"https://github.com/inhouse-work.png","language":"Ruby","readme":"# Protos\n\nA UI component library for [Phlex](https://www.phlex.fun/) using\n[tailwindcss](https://tailwindcss.com/) and\n[daisyUI](https://daisyui.com/).\n\nYou can see a full list of the components at\n[https://protos.inhouse.work/](https://protos.inhouse.work/).\n\n- Tailwindcss classes are merged using\n[tailwind\\_merge](https://github.com/gjtorikian/tailwind_merge).\n- Uses [tippy.js](https://atomiks.github.io/tippyjs/v6/getting-started/) for\n  dropdowns, combobox, and popovers\n\nOther Phlex based UI libraries worth checking out:\n\n- [PhlexUI](https://phlexui.com/)\n- [ZestUI](https://zestui.com/)\n- [Nitro Kit](https://github.com/mikker/nitro_kit)\n\nThinking of making your next static site using Phlex? Check out\n[staticky](https://github.com/nolantait/staticky). The protos docs were\npublished using it.\n\n## Phlex components\n\nPhlex is a fantastic framework for building frontend components in pure Ruby:\n\n```ruby\nclass Navbar\n  def view_template\n    header(class: \"flex items-center justify-between\") do\n      h3 { \"My site\" }\n      button { \"Log out\" }\n    end\n  end\nend\n```\n\nBut how can we sometimes render this `Navbar` with a different background color?\nWhat about the `h3`, I want that to be extra large on this one page.\n\nIt would be nice to have our components take a class like other basic elements:\n\n```ruby\nrender Navbar.new(class: \"bg-primary\")\n```\n\nUnfortunately `class` is a special keyword in Ruby, so we need to do some\nawkward handling to use it like this.\n\nThese are the kind of quality of life improvements that `protos` gives you out\nof the box. Protos makes it easy to override deep hierarchies of components\nwithout having to modify the original.\n\n## Protos::Component\n\nA protos component follows 3 conventions that make them easy to work with as\ncomponents in your app:\n\n- [Slots and themes](#slots-and-themes)\n- [Attrs and default attrs](#attrs-and-default-attrs)\n- [Params and options](#params-and-options)\n\nEvery UI component library will have a tension between being too general to fit\nin your app or too narrow to be useful. Making components that look good out of\nthe box can make them hard to customize.\n\nWe try and resolve this tension by making these components have a minimal style\nthat can be easily overridden using some ergonomic conventions.\n\n### Slots and themes\n\nComponents are styled with `css` slots that get their values from a simple hash\nwe call a `theme`.\n\n```ruby\nclass List \u003c Protos::Component\n  def view_template\n    ul(class: css[:list]) do\n      li(class: css[:item]) { \"Item 1\" }\n      li(class: css[:item]) { \"Item 2\" }\n    end\n  end\n\n  def theme\n    {\n      list: [\"space-y-4\"], # We can use arrays\n      item: \"font-bold text-2xl\" # Or just plain old strings\n    }\n  end\nend\n```\n\n\nYou define a `theme` for your component by defining a `#theme` method that\nreturns a hash.\n\nUsers of your components can override, merge, or remove parts of your theme by\npassing in their own as an argument to the component. Another nice benefit is\nthat your markup doesn't get overwhelmed horizontally with your css classes.\n\nUsing a theme and css slots allows us to easily override any part of a component\nwhen we render.\n\nHere we are passing in our own theme. The default behavior is to __add__ these\nstyles on to the theme, rather than replacing them.\n\n```ruby\nrender List.new(\n  theme: {\n    list: \"space-y-8\",\n    item: \"bg-red-500\"\n  }\n)\n```\n\nWhen the component is rendered the\n[`tailwind_merge`](https://github.com/gjtorikian/tailwind_merge)\ngem will also prune any duplicate unneeded styles.\n\nFor example even though the themes `list` key would be added together to become\n`space-y-4 space-y-8`, the `tailwind_merge` gem will prune it down to just\n`space-y-8` as the two styles conflict.\n\n```html\n\u003cul class=\"space-y-8\"\u003e\n  \u003cli class=\"font-bold text-2xl bg-red-500\"\u003eItem 1\u003c/li\u003e\n  \u003cli class=\"font-bold text-2xl bg-red-500\"\u003eItem 2\u003c/li\u003e\n\u003c/ul\u003e\n```\n\n### Overriding and negating styles\n\nWe can override the slot entirely by using a `!` at the end of the key:\n\n```ruby\nrender List.new(\n  theme: {\n    item!: \"bg-red-500\"\n  }\n)\n```\n\nThe css slot `css[:item]` would be overridden rather than merged:\n\n```html\n\u003cli class=\"bg-red-500\"\u003eItem 1\u003c/li\u003e\n```\n\nWe can also __negate__ a certain class or classes from the slot by putting a `!`\nat the __start__ of the key:\n\n```ruby\nrender List.new(\n  theme: {\n    \"!item\": \"text-2xl\"\n  }\n)\n```\n\nThe new `css[:item]` slot would be:\n\n```html\n\u003cli class=\"font-bold\"\u003eItem 1\u003c/li\u003e\n```\n\n`css` slots can also take multiple keys, and even inline styles:\n\n```ruby\nclass ListItem \u003c Protos::Component\n  def view_template\n    li(class: css[:item, :primary_item, \"text-sm\"])\n  end\nend\n```\n\nThis combines the styles together, removing any duplicates.\n\n### Attrs and default attrs\n\nBy convention, all components __should__ spread in an `attrs` hash on their\noutermost element of the component. There is no hard rule for this, but it makes\nthem feel more naturally like native html elements when you render them.\n\nBy doing this:\n\n1. We can pass a `class` keyword when initializing the component which will be\n   merged safely into the `css[:container]` slot\n2. We can pass any html attributes we want to the element like `id`, `data`\n   etc and it will __just work__\n3. We can easily add `default_attrs` that are safely merged with any provided\n   to the component when its being initialized\n\n```ruby\nclass List \u003c Protos::Component\n  def view_template\n    ul(**attrs) do\n      li { \"Item 1\"}\n    end\n  end\n\n  private\n\n  def default_attrs\n    {\n        data: { controller: \"list\" }\n    }\n  end\n\n  def theme\n    {\n      container: \"space-y-4\",\n      item: \"font-bold\"\n    }\n  end\nend\n```\n\n`#attrs` returns a hash which will by default merge the `class` keyword into the\n`css[:container]` slot. The `ul` elements class would be `space-y-4` as that is\nthe `css[:container]` on our theme.\n\nSpecial html options (`class`, `data-controller`) will be safely merged, as in\nwe won't override the components existing `data-controller` but add our own in\naddition.\n\nFor example, the component above uses a `data-controller` of `list`. If we\npassed our own controller into data when we initialize, the component's\n`data-controller` attribute would be appended.\n\n```ruby\nrender List.new(\n  data: { controller: \"tooltip\" }\n)\n```\n\nThat would output both controllers to the DOM element:\n\n```html\n\u003cul data-controller=\"list tooltip\"\u003e\n```\n\nThis makes it very convenient to add functionality to basic components without\noverriding their core behavior or having to modify/override their class.\n\nYou can also change the attributes or your theme after initialization using\n`with_attrs` and `with_theme`:\n\n```ruby\nlist = List.new\nrender list.with_attrs(id: \"my-list\").with_theme(item: \"text-red-500\")\n```\n\nThis can make it easy to pass your components as arguments to other components\nand still be able to change their styles:\n\n```ruby\nclass ListItem \u003c Protos::Component\n  param :other_component\n\n  def view_template\n    li(**attrs) do\n      render other_component.with_theme(container: \"join-item\")\n    end\n  end\n\n  def theme\n    {\n      container: \"join\"\n    }\n  end\nend\n```\n\n### Params and options\n\nComponents extend\n[`Dry::Initializer`](https://dry-rb.org/gems/dry-initializer/3.1/)\nwhich lets us easily add new positional arguments with `param` or keyword\narguments with `option` that work great with inheritance.\n\n```ruby\nclass List \u003c Protos::Component\n  option :ordered\nend\n```\n\nThis makes our initialization declarative and easy to extend without having to\nconsider how to call `super` in the initializer.\n\nThe following keywords are reserved in the base class:\n\n- `class`\n- `theme`\n- `html_options`\n\nYou are free to add whatever positional or keyword arguments you like as long as\nthey don't directly conflict with those names.\n\n## Putting it all together\n\nLets revisit the example of our `Navbar` component:\n\n```ruby\nrequire \"protos\"\n\nclass Navbar \u003c Protos::Component\n  def view_template\n    header(**attrs) do\n      h1(class: css[:heading]) { \"Hello world\" }\n      h2(class: css[:subtitle]) { \"With a subtitle\" }\n    end\n  end\n\n  private\n\n  def default_attrs\n    {\n      data: { controller: \"navbar\" }\n    }\n  end\n\n  def theme\n    {\n      container: \"flex justify-between items-center gap-sm\",\n      heading: \"text-2xl font-bold\",\n      subtitle: \"text-sm\"\n    }\n  end\nend\n```\n\nNow all the concerns about adding in our behavior, styles, etc are handled for\nus by convention:\n\n```ruby\nrender Navbar.new(\n  # This will add to the component's css[:container] slot\n  class: \"my-sm\",\n  # This will add the controller and not remove\n  # the existing one\n  data: { controller: \"counter\" },\n  theme: {\n    heading: \"p-sm\",       # We can add tokens\n    \"!container\": \"gap-sm\" # We can negate (remove) certain tokens\n    subtitle!: \"text-xl\"   # We can override the entire slot\n  }\n)\n```\n\nWhich produces the following html:\n\n```html\n\u003cheader data-controller=\"navbar counter\" class=\"flex justify-between items-center my-sm\"\u003e\n  \u003ch1 class=\"text-2xl font-bold p-sm\"\u003eHello world\u003c/h1\u003e\n  \u003ch2 class=\"text-xl\"\u003eWith a subtitle\u003c/h2\u003e\n\u003c/header\u003e\n```\n\n## Installation\n\nInstall the gem and add to the application's Gemfile by executing:\n\n    $ bundle add protos\n\nIf bundler is not being used to manage dependencies, install the gem by executing:\n\n    $ gem install protos\n\n## Usage\n\nSetup [TailwindCSS](https://tailwindcss.com/), [DaisyUI](https://daisyui.com)\nand add the protos path to your content.\n\n```\nnpm install -D tailwindcss daisyui\nnpx tailwindcss init\n```\n\nThen we need to add the protos path to the `content` of our tailwind config\nso tailwind will read the styles defined in the Protos gem. This is because the\nprotos components have tailwindcss classes which tailwind needs to be made aware\nof to bundle them in with our own css.\n\n```js\n// tailwind.config.js\nimport { execSync } from \"child_process\"\n\nconst outputProtos = execSync(\"bundle show protos\", { encoding: \"utf-8\" })\nconst protos_path = outputProtos.trim() + \"/**/*.rb\"\n\nexport default {\n  content: [\n    \"./app/views/**/*.rb\",\n    protos_path,\n  ],\n  plugins: [require(\"@tailwindcss/typography\")],\n}\n```\n\nThen in your `application.css` you can import the tailwind config:\n\n```css\n@import \"tailwindcss\";\n@config \"../../../tailwind.config.js\";\n@plugin \"daisyui\";\n```\n\nTo get the built in interactivity like dropdowns, popovers and comboboxes you\nwill need to add the `protos-stimulus` library.\n\nAdd [`protos-stimulus`](https://github.com/inhouse-work/protos-stimulus)\nto your packages:\n\n```\nnpm install protos-stimulus\n```\n\nAnd somewhere in your entrypoints import as a side effect:\n\n```js\nimport \"protos-stimulus\"\n```\n\nThen you can use the components in your apps.\n\n```ruby\nrender Protos::Card.new(class: \"bg-base-100\") do |card|\n  card.body(class: \"gap-sm\") do\n    card.title(class: \"font-bold\") { \"Hello world\" }\n    span { \"This is some more content\" }\n    card.actions do\n      button(class: \"btn btn-primary\") { \"Action 1\" }\n    end\n  end\nend\n```\n\n## Building your own components\n\n```ruby\nmodule Components\n  class Swap \u003c ApplicationComponent\n    def view_template\n      render Protos::Swap.new do |c|\n        # ....\n      end\n    end\n  end\nend\n```\n\nYou could also choose to subclass an existing component, but its usually\nrecommended to use these atoms in your own components.\n\n```ruby\nmodule Components\n  class Swap \u003c Protos::Component\n    private\n\n    def on(...)\n      MyOnButton.new(...)\n    end\n\n    def theme\n      super.merge({\n        input: [\"block\", \"bg-red-500\"]\n      })\n    end\n  end\nend\n```\n\nYou could use `Proto::List` to create your own list and even use some kind of\n[`DeferredRender`](https://www.phlex.fun/miscellaneous/v2-upgrade.html#removed-deferredrender)\nto make the API more convenient.\n\nLet's create a list component with headers and actions:\n\n```ruby\nmodule DeferredRender\n  def before_template(\u0026)\n    vanish(\u0026)\n    super\n  end\nend\n\nmodule Ui\n  class List \u003c Protos::Component\n    include Protos::Typography\n    include DeferredRender\n\n    option :title, default: -\u003e {}\n    option :ordered, default: -\u003e { false }\n    option :items, default: -\u003e { [] }\n    option :actions, default: -\u003e { [] }\n\n    def view_template\n      article(**attrs) do\n        header class: css[:header] do\n          h3(size: :md) { title }\n          nav(class: css[:actions]) do\n            @actions.each do |action|\n              render action\n            end\n          end\n        end\n\n        render Protos::List.new(ordered:, class: css[:list]) do\n          @items.each { |item| render item }\n          li(\u0026@empty) if @items.empty?\n        end\n      end\n    end\n\n    def with_item(*, **, \u0026block)\n      theme = { container: css[:item] }\n      @items \u003c\u003c Protos::List::Item.new(*, theme:, **, \u0026block)\n    end\n\n    def with_action(\u0026block)\n      @actions \u003c\u003c block\n    end\n\n    def with_empty(\u0026block)\n      @empty = block\n    end\n\n    private\n\n    def theme\n      {\n        container: \"space-y-xs\",\n        header: \"flex justify-between items-end gap-sm\",\n        list: \"divide-y border w-full\",\n        actions: \"space-x-xs\",\n        item: \"p-sm\"\n      }\n    end\n  end\nend\n```\n\nNow the component is specific to our application, and the styles are still\noverridable at all levels:\n\n```ruby\nrender Ui::List.new(title: \"Project Names\", ordered: true) do |list|\n  list.with_action { link_to(\"Add item\", \"#\") }\n  list.with_item(class: \"active\") { \"Project 1\" }\n  list.with_item { \"Project 2\" }\n  list.with_item { \"Project 3\" }\nend\n```\n\nOr here is another example of a table:\n\n```ruby\nmodule Ui\n  class Table \u003c ApplicationComponent\n    include Protos::Typography\n    include DeferredRender\n\n    class Column\n      attr_reader :title\n\n      def initialize(title, \u0026block)\n        @title = title\n        @block = block\n      end\n\n      def call(item)\n        @block.call(item)\n      end\n    end\n\n    option :title, default: -\u003e {}\n    option :collection, default: -\u003e { [] }\n    option :columns, default: -\u003e { [] }\n    option :actions, default: -\u003e { [] }\n\n    def view_template\n      article(**attrs) do\n        header class: css[:header] do\n          h3(size: :md) { title } if title.present?\n          nav(class: css[:actions]) do\n            @actions.each do |action|\n              render action\n            end\n          end\n        end\n\n        render Protos::Table.new(class: css[:table]) do |table|\n          render(table.caption(class: css[:caption]), \u0026@caption) if @caption\n          render table.header do\n            render table.row do\n              @columns.each do |column|\n                render table.head do\n                  plain(column.title)\n                end\n              end\n            end\n          end\n\n          render table.body do\n            @collection.each do |item|\n              render table.row do\n                @columns.each do |column|\n                  render table.cell do\n                    column.call(item)\n                  end\n                end\n              end\n            end\n\n            if @collection.empty?\n              render table.row do\n                render table.cell(colspan: @columns.length) do\n                  @empty\u0026.call\n                end\n              end\n            end\n          end\n        end\n      end\n    end\n\n    def with_column(...)\n      @columns \u003c\u003c Column.new(...)\n    end\n\n    def with_empty(\u0026block)\n      @empty = block\n    end\n\n    def with_caption(\u0026block)\n      @caption = block\n    end\n\n    def with_action(\u0026block)\n      @actions \u003c\u003c block\n    end\n\n    private\n\n    def theme\n      {\n        container: \"space-y-sm\",\n        header: \"flex justify-between items-end gap-sm\",\n        table: \"border\",\n        caption: \"text-muted\"\n      }\n    end\n  end\nend\n```\n\nWhich lets you have a very nice table builder:\n\n```ruby\ncollection = [\n  {\n    name: \"John Doe\",\n    status: \"Active\",\n    location: \"New York\"\n  }\n]\n\nrender Ui::Table.new(title: \"A table\", collection:) do |table|\n  table.with_caption { \"Users\" }\n  table.with_action do\n    a(href: \"#\") { \"Add new\" }\n  end\n\n  table.with_column(\"Name\") { |row| row[:name] }\n  table.with_column(\"Location\") { |row| row[:location] }\n  table.with_column(\"Status\") do |row|\n    span(class: \"badge badge-info\") { row[:status] }\n  end\n  table.with_column(\"Actions\") do\n    a(href: \"#\") { \"View\" }\n  end\nend\n```\n\n## Missing components\n\nHere is a list that we don't yet have components for:\n\n- [ ] Calendar\n- [ ] Checkbox\n- [ ] File input\n- [ ] Hover gallery\n- [ ] Indicator\n- [ ] Join\n- [ ] Kbd\n- [ ] Link\n- [ ] Loading\n- [ ] Mask\n- [ ] Progress\n- [ ] Radial progress\n- [ ] Radio\n- [ ] Range\n- [ ] Select\n- [ ] Skeleton\n- [ ] Stack\n- [ ] Text input\n- [ ] Textarea\n- [ ] Theme controller\n- [ ] Toggle\n- [ ] Tooltip\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run\n`rake spec` to run the tests. You can also run `bin/console` for an interactive\nprompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To\nrelease a new version, update the version number in `version.rb`, and then run\n`bundle exec rake release`, which will create a git tag for the version, push\ngit commits and the created tag, and push the `.gem` file to\n[rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at\nhttps://github.com/inhouse-work/protos.\n\n## Benchmarks\n\nYou can run the benchmarks using the raketasks, e.g:\n\n- `bin/rake benchmark:ips:table`\n- `bin/rake benchmark:memory:table`\n- `bin/rake benchmark:ips:theme`\n- `bin/rake benchmark:ips:attributes`\n\nThere are also tasks for profiling and exploring memory consumption.\n\nYou can find the latest benchmarks in `benchmarks/`. These were run on a new\nMacbook M3 Pro chip.\n\nCurrently this library is 30x slower than plain Phlex components. This is due to\nthe overhead of themes, attributes and other quality of life improvements.\n\nThis may seem like a lot but Phlex is so fast that rendering a large table can\nstill be done 4000 times per second with this lib.\n\n## License\n\nThe gem is available as open source under the terms of the\n[MIT License](https://opensource.org/licenses/MIT).\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finhouse-work%2Fprotos","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Finhouse-work%2Fprotos","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finhouse-work%2Fprotos/lists"}