wabi 0.3.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +157 -0
- data/LICENSE +21 -0
- data/README.md +179 -0
- data/lib/wabi/base.rb +17 -0
- data/lib/wabi/class_merge.rb +126 -0
- data/lib/wabi/generators/add_generator.rb +68 -0
- data/lib/wabi/generators/install_generator.rb +64 -0
- data/lib/wabi/generators/list_generator.rb +28 -0
- data/lib/wabi/generators/registry_generator.rb +23 -0
- data/lib/wabi/generators/theme_generator.rb +47 -0
- data/lib/wabi/lockfile.rb +42 -0
- data/lib/wabi/registry_client.rb +79 -0
- data/lib/wabi/turbo_stream_extensions.rb +52 -0
- data/lib/wabi/variants.rb +51 -0
- data/lib/wabi/version.rb +5 -0
- data/lib/wabi.rb +22 -0
- data/templates/controllers/wabi/theme_controller.js +37 -0
- data/templates/tokens.css +87 -0
- metadata +161 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: abcbfe9f2785a854e687d5f992d6ceb32c4e4fb67ba0d081416ff4bd9b383988
|
|
4
|
+
data.tar.gz: 6706563e1af726d59eafc0dbee7020a1fe565ed51cde9846020dcd74239b97e3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a367a2820c7e1276ce580cae7f577fca70d0f2f2c2f211a222f97dffc8920b1d4bbdfd3745c3ac76a921e654b74cf958b465d7fcd3501bbedfbe8aa474b8a387
|
|
7
|
+
data.tar.gz: c1de47b6186629b44e908fb3a364a564605a5b34c3551307057909fec013ae4e71b2c0a12f42e69d714f88a94a1f1ef38be755e4c1206a577edf668e469317e4
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Wabi land here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
|
+
|
|
5
|
+
## [0.3.0] — 2026-05-26
|
|
6
|
+
|
|
7
|
+
Sprint 7 (docs site polish). The docs site stops being a one-page kitchen
|
|
8
|
+
sink and starts behaving like real documentation: marketing landing,
|
|
9
|
+
components index, detailed pages for the 4 exemplar components, and
|
|
10
|
+
prose docs for onboarding / theming / philosophy. Sprint 8 (v0.4) is
|
|
11
|
+
queued to fill in the remaining 16 component detail pages + Pagefind
|
|
12
|
+
search.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`Components::Site::CodeBlock`** — Phlex `<pre><code>` wrapper with server-side Rouge syntax highlighting and a clipboard-copy button. Backed by a tiny `site--copy` Stimulus controller that hands the literal source (not the highlighted HTML) to `navigator.clipboard`.
|
|
17
|
+
- **`Components::Site::ComponentPreview`** — Tabbed Preview/Code view around an example. Block-provided live render in the Preview tab; literal source string rendered via CodeBlock in the Code tab. Backed by `site--preview-tabs` Stimulus controller that toggles panels and flips `data-active` on the tab buttons.
|
|
18
|
+
- **`ComponentsController` + `/docs/components` index** — Routes a components landing that lists all 20 v0.2 components with their manifest-derived descriptions. Cards for the 4 exemplars with detailed docs (Button / DropdownMenu / Dialog / Tabs) link out; cards for the remaining 16 show description + a "Source only" affordance until v0.4 fills them in. Nav header gains a "Components" link.
|
|
19
|
+
- **4 detailed component doc pages**: `/docs/components/button` (single-element variants), `/docs/components/dropdown_menu` (compound with submenu), `/docs/components/dialog` (overlay), `/docs/components/tabs` (in-flow navigation). Each ships: breadcrumb back-link, title, description, installation snippet, live `ComponentPreview` example, Source `CodeBlock(s)` reading the actual .rb files at request time, Accessibility bullets.
|
|
20
|
+
- **Marketing landing at `/`** — Hero ("Beautifully imperfect components for Rails") with Get-started + Browse-components CTAs, theming-aware live demo (two Cards that repaint with the theme picker), 30-second install snippet, "Why Wabi" 3-up feature grid, footer.
|
|
21
|
+
- **`/preview`** — The Sprint 1-6 kitchen sink home is preserved verbatim here for dev-time component browsing. 22 component demo sections.
|
|
22
|
+
- **`/docs/getting-started`** — 5-step onboarding (gem add → install generator → wire Tailwind 4 → mount theme controller → add components). Cross-links to /docs/components, /docs/theming, /docs/themes, /docs/philosophy.
|
|
23
|
+
- **`/docs/theming`** — How to switch palettes, available palette list, dark-mode toggle pattern, customizing tokens (HSL var space), Tailwind 4 notes (no preset.js, `@theme inline`, `@source` for autodetection).
|
|
24
|
+
- **`/docs/philosophy`** — 5-section rationale: "you own the code", Phlex-native composition, accessible-by-default, brand-neutral, Hotwire-friendly.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **Dependencies**: `docs/Gemfile` gains `rouge ~> 4.5` for server-side syntax highlighting in `CodeBlock`. No new gems beyond that.
|
|
29
|
+
- **`docs/app/views/pages/home.rb` REPLACED** with the marketing landing; the old kitchen-sink home was moved to `docs/app/views/pages/preview.rb` (class renamed `Home` → `Preview`) and routed at `/preview`.
|
|
30
|
+
- **Nav layout**: header gains a "Components" link alongside the existing "Themes" link. ThemePicker dropdown stays.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Constant resolution shadow under `Views::Pages::Components`.** Introducing the new `Views::Pages::Components::*` namespace (for the components index and 4 detailed pages) shadowed the top-level `Components::*` from anything else under `Views::Pages`. Ruby's constant lookup found `Views::Pages::Components` first and failed to find `Site` or `UI` inside it. Three views needed leading-`::` prefix on their `render` calls: `Home`, `Preview`, `Themes`. The `Themes` view (Sprint 6) had been silently broken since the components namespace was introduced — discovered and fixed in the same task.
|
|
35
|
+
|
|
36
|
+
### Spec totals
|
|
37
|
+
|
|
38
|
+
- Registry: 125/125 unchanged (Sprint 7 doesn't touch registry).
|
|
39
|
+
- Gem: 66/66 unchanged.
|
|
40
|
+
- Doc-site smoke (Rack::Test): 11/11 routes return 200 — `/`, `/preview`, `/docs/themes`, `/docs/components`, `/docs/components/{button,dropdown_menu,dialog,tabs}`, `/docs/getting-started`, `/docs/theming`, `/docs/philosophy`.
|
|
41
|
+
|
|
42
|
+
### Known v0.3 deferrals → v0.4
|
|
43
|
+
|
|
44
|
+
- **Detailed doc pages for the remaining 16 components** (input, textarea, label, card, badge, separator, alert, avatar, checkbox, switch, select, drawer, tooltip, popover, toast, accordion). The index lists them with descriptions + "Source only" affordance; v0.4 fills in the per-component pages.
|
|
45
|
+
- **Pagefind static search** — Needs Node/bun + pre-rendered HTML. Not yet worth the infrastructure for ~25 docs pages.
|
|
46
|
+
- **Sidebar nav** with active-section state — Header-only nav is fine for 5 top-level pages; sidebar makes sense once components have their own subsection.
|
|
47
|
+
- Same v0.2 deferrals still standing: `@zag-js/toast` group machine, real portal pattern, `wabi:update` generator, multi-level DropdownMenu nesting, `tailwind-merge`-equivalent class dedup edge cases, Phlex 2.4.1 Ruby 4 warnings.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## [0.2.0] — 2026-05-26
|
|
52
|
+
|
|
53
|
+
Sprint 6 (themes). 8 palettes ship, live theme switcher in the docs nav,
|
|
54
|
+
`/docs/themes` gallery page, and a `wabi:theme <slug>` generator to swap
|
|
55
|
+
themes in user apps. Sprint 7 (docs polish — per-component doc pages,
|
|
56
|
+
Pagefind search, marketing landing) is the next planned milestone.
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- **7 new theme palettes**: slate, stone, zinc, rose, blue, green, violet. Light + dark per theme, scoped via `[data-theme="<slug>"]` and `[data-theme="<slug>"][data-mode="dark"]` selectors. Default theme stays as the bare `[data-theme="default"]` (plus `:root` for the no-theme-yet case).
|
|
61
|
+
- **`bin/rails g wabi:theme <slug>` generator**: fetches `_shared.css` + `<slug>.css` from the lockfile's registry (HTTP or `file://`), concatenates, and overwrites `app/assets/tailwind/wabi/tokens.css`. Hint printed to re-run `tailwindcss:build`.
|
|
62
|
+
- **`Components::Site::ThemePicker`**: DropdownMenu in the docs nav with the 8 themes as a RadioGroup + a "Toggle dark mode" item. Wired to the existing `wabi--theme` Stimulus controller (`setTheme`/`toggleMode`), so persists in localStorage across reloads.
|
|
63
|
+
- **`/docs/themes` gallery**: side-by-side cards painting with each palette's own colors (each card carries its own `data-theme`). Each has an "Apply this theme" button that propagates the choice globally.
|
|
64
|
+
- **Registry endpoint `/r/themes/:slug.css`**: serves the per-theme CSS file. Slug regex allows the `_shared` underscore-prefixed name plus regular slugs. Realpath check guards against directory traversal.
|
|
65
|
+
- **`registry/themes/` directory + builder integration**: new canonical home for theme files. `Wabi::Registry::Builder#build_themes` runs as part of every `bin/build` and regenerates two derived artifacts:
|
|
66
|
+
- `gem/templates/tokens.css` — `_shared.css` + `default.css` only (what `wabi:install` copies into user apps).
|
|
67
|
+
- `docs/app/assets/tailwind/wabi/tokens.css` — `_shared.css` + all 8 themes concatenated (powers live switching in the docs site).
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **DropdownMenuItem / DropdownMenuRadioItem dropped user-supplied `data:` kwargs.** Both components extracted only `:class` from `@attrs`, silently discarding everything else. Surfaced when wiring ThemePicker's `data: { action: "click->wabi--theme#setTheme", ... }` and the action attribute never reached the DOM. Both components now merge user data with component data; component keys win on collision so caller-supplied attrs can't override target/value bindings. Two new regression specs lock this in.
|
|
72
|
+
- **CSS specificity bug: `:root` redefined per theme clobbered light-mode switching.** First pass had every theme file start with `:root, [data-theme="<slug>"]`. When concatenated for the docs site, `:root` appeared 8 times. Since `:root` and `[data-theme="X"]` have equal specificity (0,1,0), the later `:root` block (from violet, last in concat order) overrode every earlier `[data-theme="X"]` rule — so every light-mode theme repainted as violet. Dark mode escaped because `[data-theme="X"][data-mode="dark"]` has specificity (0,2,0). Fix: only `default.css` keeps the `:root, ` prefix; the other 7 themes use `[data-theme="<slug>"]` alone. Regression spec asserts exactly one `:root` block in the docs output.
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
|
|
76
|
+
- **Default theme location moved** from `gem/templates/tokens.css` (hand-maintained) to `registry/themes/default.css` (canonical source) + `_shared.css` (boilerplate). The gem template is now a build artifact, regenerated by `bin/build`. Users see no functional difference; gem upgrades carry the same default palette as before.
|
|
77
|
+
- **Site::Layout grew a `<header>`** with a "Wabi" home link, a "Themes" nav link to `/docs/themes`, and the theme picker. The `raw safe(yield_content(&block))` + `<Toaster>` body siblings are preserved exactly.
|
|
78
|
+
|
|
79
|
+
### Spec totals
|
|
80
|
+
|
|
81
|
+
- Registry: 125 examples, 0 failures (was 119 at v0.1; +6 across `#build_themes` describe block + DropdownMenu user-data merge specs).
|
|
82
|
+
- Gem: 66 examples, 0 failures (was 64; +2 from `theme_generator_spec`).
|
|
83
|
+
|
|
84
|
+
### Known v0.2 deferrals → v0.3
|
|
85
|
+
|
|
86
|
+
- Pagefind static search.
|
|
87
|
+
- Per-component documentation pages (20 routes).
|
|
88
|
+
- Marketing-oriented landing page; kitchen-sink home would move to `/preview`.
|
|
89
|
+
- Getting-started / philosophy / theming concept prose pages.
|
|
90
|
+
- `@zag-js/toast` group machine (max stack, gap/offset, pause-on-group-hover).
|
|
91
|
+
- Real portal pattern for transformed ancestors.
|
|
92
|
+
- `wabi:update` diff-aware generator (`wabi:install --force` is the current minimum).
|
|
93
|
+
- Multi-level DropdownMenu nesting (v0.2 still supports single-level submenus).
|
|
94
|
+
- Phlex 2.4.1 Ruby 4 warnings (wait for upstream fix).
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## [0.1.0] — 2026-05-26
|
|
99
|
+
|
|
100
|
+
First milestone release. 20 components, accessible, themable, no Zag.js
|
|
101
|
+
0.x quirks left, no public gem / GitHub repo yet.
|
|
102
|
+
|
|
103
|
+
### Components (20)
|
|
104
|
+
|
|
105
|
+
- **Static** (9): Button, Input, Textarea, Label, Card (compound), Badge, Separator, Alert (compound), Avatar (compound).
|
|
106
|
+
- **Forms** (3): Checkbox, Switch, Select (compound).
|
|
107
|
+
- **Overlays** (4): Dialog (compound), Drawer (4-side variant), Tooltip, Popover.
|
|
108
|
+
- **Menus + feedback** (2): DropdownMenu (with nested Submenu + checkbox/radio items), Toast (+ Toaster singleton container).
|
|
109
|
+
- **Navigation** (2): Tabs, Accordion.
|
|
110
|
+
|
|
111
|
+
### Gem
|
|
112
|
+
|
|
113
|
+
- `wabi:install` + `wabi:add` + `wabi:list` + `wabi:registry` Rails generators.
|
|
114
|
+
- `Wabi::Base` Phlex base class with CVA-style `variants` DSL and variant-aware `merge_class` Tailwind dedup (`focus-visible:`, `data-[state=…]:`, axis-aware, width-vs-color for `border`/`ring`/`text`/`outline`/`divide`).
|
|
115
|
+
- `Wabi::RegistryClient` with disk cache, dev-context cache bypass (file://, localhost).
|
|
116
|
+
- `Wabi::Lockfile` tracking installed components.
|
|
117
|
+
- `Wabi::TurboStreamExtensions` adds `turbo_stream.wabi_toast(...)` when Turbo is loaded.
|
|
118
|
+
- Tailwind 4 native — `@theme inline` + `@custom-variant dark`, no preset.js.
|
|
119
|
+
|
|
120
|
+
### Registry
|
|
121
|
+
|
|
122
|
+
- Build pipeline produces `dist/r/<name>.json` per component + `index.json`.
|
|
123
|
+
- JSON Schema validation (`schema/component.v1.json`).
|
|
124
|
+
- 20 components installable via `bin/rails g wabi:add <name>`.
|
|
125
|
+
|
|
126
|
+
### Accessibility
|
|
127
|
+
|
|
128
|
+
- All overlays toggle the `inert` attribute via Zag's `onOpenChange` callback (synchronous inside the state transition, lands before `setInitialFocus`). Closed overlays are out of the tab order and the accessibility tree.
|
|
129
|
+
- `data-state` driven CSS transitions (opacity / translate / pointer-events) — no `display:none` cuts transitions short.
|
|
130
|
+
- WCAG-AA targeted per component manifest.
|
|
131
|
+
|
|
132
|
+
### Theming
|
|
133
|
+
|
|
134
|
+
- Default theme ships in `app/assets/tailwind/wabi/tokens.css` via `wabi:install`.
|
|
135
|
+
- `data-mode="dark"` toggles dark palette; `data-theme="…"` ready for multi-palette in v0.2.
|
|
136
|
+
|
|
137
|
+
### Tooling
|
|
138
|
+
|
|
139
|
+
- `bin/dev` monorepo entrypoint clears stale `docs/tmp/pids/server.pid` on launch.
|
|
140
|
+
- CI verifies all 20 dist artifacts on push (`.github/workflows/registry-build.yml`).
|
|
141
|
+
- Registry spec suite: 119 examples, 0 failures.
|
|
142
|
+
- Gem spec suite: 64 examples, 0 failures.
|
|
143
|
+
|
|
144
|
+
### Documentation
|
|
145
|
+
|
|
146
|
+
- `docs/SMOKE-TEST.md` records per-sprint outcomes and Zag.js 1.x trap memos (entries 1–10 in `feedback_zag_js_pattern`).
|
|
147
|
+
- `docs/V01-CARRYOVER.md` tracks v0.1 carryover (now closed) plus v0.2 deferrals.
|
|
148
|
+
|
|
149
|
+
### Known v0.2 deferrals
|
|
150
|
+
|
|
151
|
+
- `@zag-js/toast` group machine (`max` / `gap` / pause-on-group-hover). Current vanilla controller per toast covers the 99% case.
|
|
152
|
+
- Real portal pattern (Stimulus loses target tracking when subtree moves to `document.body`). Workaround: `position: fixed` + high z-index.
|
|
153
|
+
- Multi-palette theme picker UI + documented theme-extension flow.
|
|
154
|
+
- `wabi:update` diff-aware generator (current `wabi:install --force` is the minimum acceptable for v0.1).
|
|
155
|
+
- Multi-level DropdownMenu nesting (sub-inside-a-sub). v0.1 supports single-level submenus.
|
|
156
|
+
- `tailwind-merge`-equivalent class dedup for edge cases (`bg-cover` collapsing under `bg-{color}` etc.).
|
|
157
|
+
- Phlex 2.4.1 emits Ruby 4.0.5 warnings at load time; wait for upstream fix.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oscar Ortega and Wabi contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Wabi
|
|
2
|
+
|
|
3
|
+
> Beautifully imperfect components for Rails.
|
|
4
|
+
|
|
5
|
+
Wabi is an open-source UI component library for **Ruby on Rails 8**, built on **Phlex + Tailwind 4 + Stimulus + Hotwire**. Inspired by shadcn/ui, components are *copied* into your app — you own the code, customize freely, no upstream API to drift away from.
|
|
6
|
+
|
|
7
|
+
🎉 **Status:** v0.3.0 alpha. 20 components, 8 theme palettes, WCAG-AA targeted. Not yet on RubyGems.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 1. Add the gem (path-based for now; RubyGems push pending)
|
|
15
|
+
bundle add wabi --git https://github.com/wabikit/wabi --glob 'gem/*'
|
|
16
|
+
|
|
17
|
+
# 2. Run the installer (copies tokens.css + theme controller + lockfile)
|
|
18
|
+
bin/rails g wabi:install
|
|
19
|
+
|
|
20
|
+
# 3. Add components from the registry
|
|
21
|
+
bin/rails g wabi:add button card dialog
|
|
22
|
+
|
|
23
|
+
# 4. Render
|
|
24
|
+
# In any Phlex view:
|
|
25
|
+
# render Components::UI::Button.new(appearance: :primary) { "Click me" }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then add `@import "./wabi/tokens.css";` AFTER `@import "tailwindcss";` in your `app/assets/tailwind/application.css`, and mount `data-controller="wabi--theme"` on `<html>` in your layout.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## What's in the box
|
|
33
|
+
|
|
34
|
+
### 20 components
|
|
35
|
+
|
|
36
|
+
| Static (9) | Forms (3) | Overlays (4) | Menus + Feedback (2) | Navigation (2) |
|
|
37
|
+
|---|---|---|---|---|
|
|
38
|
+
| Button | Checkbox | Dialog | DropdownMenu | Tabs |
|
|
39
|
+
| Input | Switch | Drawer (4 sides) | Toast + Toaster | Accordion |
|
|
40
|
+
| Textarea | Select | Tooltip | | |
|
|
41
|
+
| Label | | Popover | | |
|
|
42
|
+
| Card (compound) | | | | |
|
|
43
|
+
| Badge | | | | |
|
|
44
|
+
| Separator | | | | |
|
|
45
|
+
| Alert (compound) | | | | |
|
|
46
|
+
| Avatar (compound) | | | | |
|
|
47
|
+
|
|
48
|
+
All interactive components wire through **Zag.js 1.x** state machines for WAI-ARIA roles, keyboard semantics, and focus management. Toast uses a custom vanilla controller (cross-toast coordination is a v0.4 task).
|
|
49
|
+
|
|
50
|
+
### 8 theme palettes
|
|
51
|
+
|
|
52
|
+
`default`, `slate`, `stone`, `zinc`, `rose`, `blue`, `green`, `violet`. Light + dark variants per theme. Switch with:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
bin/rails g wabi:theme rose
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or live-switch via the `wabi--theme` Stimulus controller (sets `data-theme` on `<html>`, persists in `localStorage`).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Why Wabi
|
|
63
|
+
|
|
64
|
+
| Principle | What it means |
|
|
65
|
+
|---|---|
|
|
66
|
+
| **You own the code** | `bin/rails g wabi:add button` COPIES the Phlex source into your app. Edit, refactor, fork — there's no upstream API to drift from, because the upstream is you. |
|
|
67
|
+
| **Phlex-native** | Components are Ruby classes. Composition is method dispatch. Variants are class-method DSLs. Real inheritance, IDE tab-into-source, like the rest of your Rails app. |
|
|
68
|
+
| **Accessible by default** | Zag.js carries WAI-ARIA roles, keyboard nav, focus management, scroll lock for modals. Overlays toggle `inert` when closed so they stay out of tab order + the a11y tree. |
|
|
69
|
+
| **Brand-neutral** | 8 carefully-chosen palettes. None is "the Wabi look". Pick the closest one or edit HSL values directly. |
|
|
70
|
+
| **Hotwire-friendly** | Stimulus controllers wrap each Zag machine. `turbo_stream.wabi_toast(...)` lets the server spawn notifications without round-tripping the page. |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Example: a confirmation dialog
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# app/views/users/destroy_confirmation.rb
|
|
78
|
+
class Views::Users::DestroyConfirmation < Views::Base
|
|
79
|
+
def view_template
|
|
80
|
+
render Components::UI::Dialog.new do
|
|
81
|
+
render Components::UI::DialogTrigger.new(
|
|
82
|
+
class: "inline-flex h-10 px-4 rounded-md bg-destructive text-destructive-foreground"
|
|
83
|
+
) { "Delete account" }
|
|
84
|
+
|
|
85
|
+
render Components::UI::DialogContent.new do
|
|
86
|
+
render Components::UI::DialogHeader.new do
|
|
87
|
+
render Components::UI::DialogTitle.new { "Delete account" }
|
|
88
|
+
render Components::UI::DialogDescription.new { "This action cannot be undone." }
|
|
89
|
+
end
|
|
90
|
+
render Components::UI::DialogFooter.new do
|
|
91
|
+
render Components::UI::DialogCancel.new { "Cancel" }
|
|
92
|
+
render Components::UI::DialogAction.new(appearance: :destructive,
|
|
93
|
+
data: { action: "click->wabi--dialog#close" }) { "Delete" }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
That's a fully-accessible modal with focus trap, scroll lock, backdrop click, Escape dismiss, and `inert` on close — out of the box. The Phlex source is in `app/components/ui/dialog*.rb`; modify whatever you want.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## CLI reference
|
|
106
|
+
|
|
107
|
+
| Generator | What it does |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `wabi:install [--force]` | Copies `tokens.css`, the `wabi--theme` Stimulus controller, and initializes `config/wabi.lock.json`. `--force` re-copies tokens/controller on gem upgrades (lockfile is preserved). |
|
|
110
|
+
| `wabi:add <name…>` | Copies one or more component source files from the registry into `app/components/ui/` and their controllers into `app/javascript/controllers/wabi/`. Updates the lockfile. |
|
|
111
|
+
| `wabi:list` | Lists all available components in the configured registry. |
|
|
112
|
+
| `wabi:registry <url>` | Switches the active registry origin (default: `https://wabikit.dev/r`). |
|
|
113
|
+
| `wabi:theme <slug>` | Swaps `tokens.css` for the requested palette. Run `bin/rails tailwindcss:build` after. |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Compatibility
|
|
118
|
+
|
|
119
|
+
- **Ruby**: 3.4 or later
|
|
120
|
+
- **Rails**: 8.0 or later
|
|
121
|
+
- **Tailwind**: 4.x (native `@theme inline`, no `tailwind.config.js`/`preset.js`)
|
|
122
|
+
- **Phlex**: 2.4 or later
|
|
123
|
+
- **Stimulus**: 3.x
|
|
124
|
+
- **Browsers**: Chrome 117+, Safari 17.4+, Firefox 119+ (some components use modern CSS like `grid-template-rows` height animation for Accordion)
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Documentation
|
|
129
|
+
|
|
130
|
+
The full docs site is at the GitHub repo's `docs/` Rails app (also planned to host at https://wabikit.dev when DNS is wired). Locally:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
git clone https://github.com/wabikit/wabi
|
|
134
|
+
cd wabi
|
|
135
|
+
bin/dev # starts registry watcher + tailwind watcher + docs server on :3000
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Then visit:
|
|
139
|
+
- `/` — marketing landing
|
|
140
|
+
- `/docs/components` — index of all 20 components
|
|
141
|
+
- `/docs/components/{button,dropdown_menu,dialog,tabs}` — detailed pages with live preview + source
|
|
142
|
+
- `/docs/themes` — all 8 palettes side-by-side
|
|
143
|
+
- `/docs/getting-started`, `/docs/theming`, `/docs/philosophy` — prose docs
|
|
144
|
+
- `/preview` — the Sprint 1-6 kitchen sink (every component on one page)
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Monorepo layout
|
|
149
|
+
|
|
150
|
+
- `gem/` — the `wabi` Ruby gem: runtime (`Wabi::Base`, `Wabi::Variants`, `Wabi::ClassMerge`, `Wabi::RegistryClient`, `Wabi::Lockfile`) + Rails generators.
|
|
151
|
+
- `registry/` — component source files (`components/<name>/{manifest.yml, *.rb, *.js}`) + theme CSS files + the build pipeline emitting `dist/r/<name>.json`.
|
|
152
|
+
- `docs/` — the `wabikit.dev` Rails app; also serves the registry at `/r/*.json` and `/r/themes/*.css`.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Roadmap
|
|
157
|
+
|
|
158
|
+
| Version | Target | Status |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| v0.1 | 20 components | ✅ shipped 2026-05-26 |
|
|
161
|
+
| v0.2 | 8 themes + theme picker | ✅ shipped 2026-05-26 |
|
|
162
|
+
| v0.3 | Real docs site (marketing + components index + 4 detailed pages + prose) | ✅ shipped 2026-05-26 |
|
|
163
|
+
| v0.4 | Detailed pages for remaining 16 components; Pagefind search; sidebar nav | planned |
|
|
164
|
+
| v0.5 | `@zag-js/toast` group machine; real portal pattern; `wabi:update` generator | planned |
|
|
165
|
+
| v1.0 | API stability; external a11y audit | 2027-04 target |
|
|
166
|
+
|
|
167
|
+
See [ROADMAP.md](./ROADMAP.md) for the long-term view and [CHANGELOG.md](./CHANGELOG.md) for the per-release detail.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Contributing
|
|
172
|
+
|
|
173
|
+
Wabi is in alpha and the API is still moving. Filing issues with concrete repros, suggestions for components, or theme palette ideas is the most useful kind of contribution right now. See [CONTRIBUTING.md](./CONTRIBUTING.md) (TODO — will land before v0.4) for the per-component anatomy and the Zag.js wiring conventions used throughout.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT — see [LICENSE](./LICENSE). Theme HSL values derive from [shadcn/ui](https://ui.shadcn.com)'s palettes, also MIT-licensed.
|
data/lib/wabi/base.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "phlex"
|
|
4
|
+
require_relative "variants"
|
|
5
|
+
require_relative "class_merge"
|
|
6
|
+
|
|
7
|
+
module Wabi
|
|
8
|
+
class Base < Phlex::HTML
|
|
9
|
+
extend Wabi::Variants
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def merge_class(*classes)
|
|
14
|
+
Wabi::ClassMerge.call(*classes)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Wabi
|
|
6
|
+
# Minimal Tailwind class deduplication. Later tokens win for the same group key.
|
|
7
|
+
#
|
|
8
|
+
# The group key is variant-aware: any variants stacked in front of a utility
|
|
9
|
+
# (e.g. "focus-visible:", "data-[state=checked]:", "md:dark:") become part of
|
|
10
|
+
# the key, so variant-scoped utilities never collide with their plain
|
|
11
|
+
# counterparts. Inside a given variant scope, the key is the utility's first
|
|
12
|
+
# hyphen-separated segment ("h-4" → "h", "bg-red-500" → "bg").
|
|
13
|
+
#
|
|
14
|
+
# NOTE: Still intentionally minimal for v0.1 — utilities that share a prefix
|
|
15
|
+
# but represent different CSS properties (e.g. `ring-2` width vs `ring-ring`
|
|
16
|
+
# color, or `border` width vs `border-primary` color) collide and the later
|
|
17
|
+
# one wins. Full Tailwind conflict resolution (à la tailwind-merge) is
|
|
18
|
+
# post-v0.1.
|
|
19
|
+
module ClassMerge
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def call(*inputs)
|
|
23
|
+
tokens = inputs.compact.flat_map { |s| s.to_s.split(/\s+/) }.reject(&:empty?)
|
|
24
|
+
seen = {}
|
|
25
|
+
tokens.each do |token|
|
|
26
|
+
seen[group_key(token)] = token
|
|
27
|
+
end
|
|
28
|
+
seen.values.join(" ")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def group_key(token)
|
|
32
|
+
idx = last_colon_outside_brackets(token)
|
|
33
|
+
if idx
|
|
34
|
+
prefix = token[0..idx]
|
|
35
|
+
utility = token[(idx + 1)..]
|
|
36
|
+
else
|
|
37
|
+
prefix = ""
|
|
38
|
+
utility = token
|
|
39
|
+
end
|
|
40
|
+
"#{prefix}#{utility_group(utility)}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Single-word "atom" utilities that name a CSS property whose compound
|
|
44
|
+
# siblings (e.g. `flex` vs `flex-col`, `border` vs `border-input`) target
|
|
45
|
+
# an entirely different property and must NOT share a dedup bucket. Without
|
|
46
|
+
# this distinction, `flex flex-col` collapses to just `flex-col` -- the
|
|
47
|
+
# display:flex rule is lost and the children don't lay out as a flex row.
|
|
48
|
+
ATOM_UTILITIES = %w[
|
|
49
|
+
flex grid block inline hidden visible
|
|
50
|
+
border ring rounded outline
|
|
51
|
+
transition truncate
|
|
52
|
+
absolute relative fixed static sticky
|
|
53
|
+
].to_set.freeze
|
|
54
|
+
|
|
55
|
+
# Directional / axial suffix segments that distinguish utilities in the
|
|
56
|
+
# same family (e.g. `-translate-x-1/2` vs `-translate-y-1/2` are different
|
|
57
|
+
# axes, `border-l` vs `border-r` are different sides). When the first
|
|
58
|
+
# segment after the family root matches one of these, keep the family root
|
|
59
|
+
# PLUS the direction segment as the group key.
|
|
60
|
+
AXIS_FAMILIES = %w[translate -translate scale skew rotate space border].to_set.freeze
|
|
61
|
+
AXIS_SUFFIXES = %w[x y z t b l r s e].to_set.freeze
|
|
62
|
+
|
|
63
|
+
# Families whose first non-family segment can be EITHER a width/size value
|
|
64
|
+
# OR a color name. Tailwind treats these as different CSS properties so
|
|
65
|
+
# they must dedup independently. Without this, `border-2 border-input`
|
|
66
|
+
# collapses to just `border-input` (loses the width), or `ring-2 ring-ring
|
|
67
|
+
# ring-offset-2` collapses to one of them (the focus ring vanishes).
|
|
68
|
+
WIDTH_COLOR_FAMILIES = %w[border ring text outline divide].to_set.freeze
|
|
69
|
+
|
|
70
|
+
# Tail tokens that unambiguously identify a size/length utility. Anything
|
|
71
|
+
# that doesn't match these AND doesn't look numeric is assumed to be a
|
|
72
|
+
# color (or other named theme token).
|
|
73
|
+
SIZE_TOKENS = %w[
|
|
74
|
+
xs sm base md lg xl 2xl 3xl 4xl 5xl 6xl 7xl 8xl 9xl
|
|
75
|
+
auto full screen min max fit none px
|
|
76
|
+
].to_set.freeze
|
|
77
|
+
|
|
78
|
+
def utility_group(utility)
|
|
79
|
+
return "atom:#{utility}" if ATOM_UTILITIES.include?(utility)
|
|
80
|
+
|
|
81
|
+
segments = utility.split("-").reject(&:empty?)
|
|
82
|
+
head = utility.start_with?("-") ? "-#{segments.first}" : segments.first
|
|
83
|
+
tail = utility.start_with?("-") ? segments[1] : segments[1]
|
|
84
|
+
|
|
85
|
+
# `ring-offset-*` is its own family: `ring-offset-2` (width) and
|
|
86
|
+
# `ring-offset-input` (color) must NOT collide with bare `ring-*`.
|
|
87
|
+
if head == "ring" && tail == "offset"
|
|
88
|
+
sub_tail = utility.start_with?("-") ? segments[2] : segments[2]
|
|
89
|
+
return "ring-offset:size" if sub_tail && size_or_numeric?(sub_tail)
|
|
90
|
+
return "ring-offset:color" if sub_tail
|
|
91
|
+
return "ring-offset"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return "#{head}-#{tail}" if AXIS_FAMILIES.include?(head) && AXIS_SUFFIXES.include?(tail)
|
|
95
|
+
|
|
96
|
+
if WIDTH_COLOR_FAMILIES.include?(head) && tail
|
|
97
|
+
return "#{head}:size" if size_or_numeric?(tail)
|
|
98
|
+
return "#{head}:color"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
head
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def size_or_numeric?(tail)
|
|
105
|
+
return true if SIZE_TOKENS.include?(tail)
|
|
106
|
+
return true if tail.match?(/\A-?\d+(\/\d+)?\z/)
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The last `:` that lives outside any `[...]` block. Arbitrary-value variants
|
|
111
|
+
# like `data-[state=checked]:` may contain `:` inside the brackets, which
|
|
112
|
+
# must not be treated as the variant→utility separator.
|
|
113
|
+
def last_colon_outside_brackets(token)
|
|
114
|
+
depth = 0
|
|
115
|
+
idx = nil
|
|
116
|
+
token.each_char.with_index do |c, i|
|
|
117
|
+
case c
|
|
118
|
+
when "[" then depth += 1
|
|
119
|
+
when "]" then depth -= 1
|
|
120
|
+
when ":" then idx = i if depth.zero?
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
idx
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "wabi/registry_client"
|
|
6
|
+
require "wabi/lockfile"
|
|
7
|
+
|
|
8
|
+
module Wabi
|
|
9
|
+
module Generators
|
|
10
|
+
class AddGenerator < Rails::Generators::Base
|
|
11
|
+
argument :components, type: :array, banner: "component_name [component_name ...]"
|
|
12
|
+
|
|
13
|
+
desc "Add one or more Wabi components to this app."
|
|
14
|
+
|
|
15
|
+
def add_components
|
|
16
|
+
@js_deps_to_pin = {}
|
|
17
|
+
lockfile # eager-init via accessor
|
|
18
|
+
components.each { |name| install_component(name) }
|
|
19
|
+
lockfile.save
|
|
20
|
+
print_js_pin_instructions
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def lockfile
|
|
26
|
+
@lockfile ||= Wabi::Lockfile.load(File.join(destination_root, "config/wabi.lock.json"))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def client
|
|
30
|
+
@client ||= Wabi::RegistryClient.new(base_url: lockfile.registry)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def install_component(name)
|
|
34
|
+
return if lockfile.components.key?(name)
|
|
35
|
+
say " fetching #{name}", :cyan
|
|
36
|
+
data = client.fetch(name)
|
|
37
|
+
|
|
38
|
+
Array(data["registry_dependencies"]).each { |dep| install_component(dep) }
|
|
39
|
+
|
|
40
|
+
data["files"].each do |file|
|
|
41
|
+
target = File.join(destination_root, file["path"])
|
|
42
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
43
|
+
File.write(target, file["content"])
|
|
44
|
+
say " create #{file["path"]}", :green
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
(data["js_dependencies"] || {}).each { |pkg, ver| @js_deps_to_pin[pkg] = ver }
|
|
48
|
+
|
|
49
|
+
hash = Digest::SHA256.hexdigest(JSON.generate(data["files"]))
|
|
50
|
+
lockfile.record(name, version: data["version"], hash: hash)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def print_js_pin_instructions
|
|
54
|
+
return if @js_deps_to_pin.empty?
|
|
55
|
+
say "\n This component requires JS packages. Add these pins to config/importmap.rb:", :yellow
|
|
56
|
+
@js_deps_to_pin.each do |pkg, version|
|
|
57
|
+
v = version.to_s.sub(/\A[~^]/, "")
|
|
58
|
+
v = "1.0.0" if v.empty?
|
|
59
|
+
say %( pin "#{pkg}", to: "https://cdn.jsdelivr.net/npm/#{pkg}@#{v}/+esm")
|
|
60
|
+
end
|
|
61
|
+
say "\n NOTE: `bin/importmap pin <pkg>` is NOT recommended for packages with submodule"
|
|
62
|
+
say " imports (like @zag-js/*) — it only downloads the entry file, leaving relative"
|
|
63
|
+
say " imports unresolved. The `+esm` endpoint above ships a single bundle with all"
|
|
64
|
+
say " transitive deps resolved to absolute URLs.", :yellow
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "wabi/lockfile"
|
|
5
|
+
|
|
6
|
+
module Wabi
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path("../../../templates", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Set up Wabi in this Rails app: copy tokens, theme controller, init lockfile."
|
|
12
|
+
|
|
13
|
+
class_option :force, type: :boolean, default: false,
|
|
14
|
+
desc: "Overwrite existing tokens.css and theme_controller.js (the lockfile is preserved)."
|
|
15
|
+
|
|
16
|
+
def copy_tokens
|
|
17
|
+
copy_file "tokens.css", "app/assets/tailwind/wabi/tokens.css",
|
|
18
|
+
force: options[:force], skip: !options[:force]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def copy_theme_controller
|
|
22
|
+
copy_file "controllers/wabi/theme_controller.js",
|
|
23
|
+
"app/javascript/controllers/wabi/theme_controller.js",
|
|
24
|
+
force: options[:force], skip: !options[:force]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def init_lockfile
|
|
28
|
+
path = File.join(destination_root, "config/wabi.lock.json")
|
|
29
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
30
|
+
return if File.exist?(path)
|
|
31
|
+
lockfile = Wabi::Lockfile.load(path)
|
|
32
|
+
lockfile.save
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def print_next_steps
|
|
36
|
+
say "\n Wabi installed. Next steps:", :green
|
|
37
|
+
say ""
|
|
38
|
+
say " 1. Import tokens AFTER the Tailwind import in app/assets/tailwind/application.css:"
|
|
39
|
+
say " @import \"tailwindcss\";"
|
|
40
|
+
say " @import \"./wabi/tokens.css\";"
|
|
41
|
+
say ""
|
|
42
|
+
say " 2. Use 'tailwind' (not 'application') in your stylesheet link tag:"
|
|
43
|
+
say " stylesheet_link_tag \"tailwind\", \"data-turbo-track\": \"reload\""
|
|
44
|
+
say " (Rails 8.1's :app symbol also works if you keep the default ERB layout.)"
|
|
45
|
+
say ""
|
|
46
|
+
say " 3. Mount the theme controller on <html> in your layout:"
|
|
47
|
+
say " <html data-controller=\"wabi--theme\">"
|
|
48
|
+
say ""
|
|
49
|
+
say " 4. Add components from the registry:"
|
|
50
|
+
say " bin/rails g wabi:add button input card"
|
|
51
|
+
say " Components autoload under Components::UI::* (Phlex 2.x convention)."
|
|
52
|
+
say " Render them as: render Components::UI::Button.new"
|
|
53
|
+
say ""
|
|
54
|
+
say " 5. If Tailwind doesn't pick up component classes, add this near the top of"
|
|
55
|
+
say " application.css (Tailwind 4 normally auto-detects .rb files, but in"
|
|
56
|
+
say " unusual asset paths an explicit @source helps):"
|
|
57
|
+
say " @source \"../../components/**/*.rb\";"
|
|
58
|
+
say ""
|
|
59
|
+
say " Run wabi:install --force later to refresh tokens.css and theme_controller.js"
|
|
60
|
+
say " after a gem upgrade (your wabi.lock.json is preserved)."
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "wabi/lockfile"
|
|
5
|
+
require "wabi/registry_client"
|
|
6
|
+
|
|
7
|
+
module Wabi
|
|
8
|
+
module Generators
|
|
9
|
+
class ListGenerator < Rails::Generators::Base
|
|
10
|
+
desc "List installed Wabi components and available components in the registry."
|
|
11
|
+
|
|
12
|
+
def list
|
|
13
|
+
lockfile = Wabi::Lockfile.load(File.join(destination_root, "config/wabi.lock.json"))
|
|
14
|
+
installed_names = lockfile.components.keys
|
|
15
|
+
|
|
16
|
+
client = Wabi::RegistryClient.new(base_url: lockfile.registry)
|
|
17
|
+
catalog = client.fetch("index")["components"] || []
|
|
18
|
+
|
|
19
|
+
say "\nRegistry: #{lockfile.registry}\n", :cyan
|
|
20
|
+
catalog.each do |entry|
|
|
21
|
+
name = entry["name"]
|
|
22
|
+
marker = installed_names.include?(name) ? "[installed]" : " "
|
|
23
|
+
say format(" %s %-20s v%s", marker, name, entry["version"])
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "wabi/lockfile"
|
|
5
|
+
|
|
6
|
+
module Wabi
|
|
7
|
+
module Generators
|
|
8
|
+
class RegistryGenerator < Rails::Generators::Base
|
|
9
|
+
argument :url, type: :string, banner: "registry_url"
|
|
10
|
+
|
|
11
|
+
desc "Set the registry URL for this project."
|
|
12
|
+
|
|
13
|
+
def update_registry
|
|
14
|
+
path = File.join(destination_root, "config/wabi.lock.json")
|
|
15
|
+
lockfile = Wabi::Lockfile.load(path)
|
|
16
|
+
# Lockfile doesn't currently expose a registry setter; bypass via instance_var.
|
|
17
|
+
lockfile.instance_variable_set(:@registry, url)
|
|
18
|
+
lockfile.save
|
|
19
|
+
say " registry → #{url}", :green
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "wabi/lockfile"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Wabi
|
|
9
|
+
module Generators
|
|
10
|
+
class ThemeGenerator < Rails::Generators::Base
|
|
11
|
+
argument :slug, type: :string, banner: "theme_slug"
|
|
12
|
+
|
|
13
|
+
desc "Switch to a different Wabi theme palette (default, slate, stone, zinc, rose, blue, green, violet)."
|
|
14
|
+
|
|
15
|
+
def fetch_and_write
|
|
16
|
+
lockfile = Wabi::Lockfile.load(File.join(destination_root, "config/wabi.lock.json"))
|
|
17
|
+
base = lockfile.registry # e.g. https://wabikit.dev/r OR file:///abs/path
|
|
18
|
+
|
|
19
|
+
shared = fetch("#{base}/themes/_shared.css", label: "_shared")
|
|
20
|
+
body = fetch("#{base}/themes/#{slug}.css", label: slug)
|
|
21
|
+
|
|
22
|
+
target = File.join(destination_root, "app/assets/tailwind/wabi/tokens.css")
|
|
23
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
24
|
+
File.write(target, [shared, body].join("\n"))
|
|
25
|
+
|
|
26
|
+
say " theme → #{slug}", :green
|
|
27
|
+
say " wrote app/assets/tailwind/wabi/tokens.css", :green
|
|
28
|
+
say " Run `bin/rails tailwindcss:build` to compile the new palette.", :cyan
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def fetch(url_or_path, label:)
|
|
34
|
+
if url_or_path.start_with?("file://")
|
|
35
|
+
path = url_or_path.sub("file://", "")
|
|
36
|
+
raise Wabi::Error, "Theme #{label} not found at #{path}" unless File.exist?(path)
|
|
37
|
+
File.read(path)
|
|
38
|
+
else
|
|
39
|
+
uri = URI.parse(url_or_path)
|
|
40
|
+
response = Net::HTTP.get_response(uri)
|
|
41
|
+
raise Wabi::Error, "Theme #{label} not found at #{url_or_path}: HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
42
|
+
response.body
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Wabi
|
|
7
|
+
# Manages config/wabi.lock.json in a user's Rails app.
|
|
8
|
+
# Tracks installed components, versions, hashes, and registry origin.
|
|
9
|
+
class Lockfile
|
|
10
|
+
DEFAULT_REGISTRY = "https://wabikit.dev/r"
|
|
11
|
+
|
|
12
|
+
attr_reader :path, :registry, :components
|
|
13
|
+
|
|
14
|
+
def self.load(path)
|
|
15
|
+
data =
|
|
16
|
+
if File.exist?(path)
|
|
17
|
+
JSON.parse(File.read(path))
|
|
18
|
+
else
|
|
19
|
+
{}
|
|
20
|
+
end
|
|
21
|
+
new(path: path, data: data)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(path:, data: {})
|
|
25
|
+
@path = path
|
|
26
|
+
@registry = data["registry"] || DEFAULT_REGISTRY
|
|
27
|
+
@components = data["components"] || {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def record(name, version:, hash:)
|
|
31
|
+
@components[name] = { "version" => version, "hash" => hash }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def save
|
|
35
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
36
|
+
File.write(@path, JSON.pretty_generate({
|
|
37
|
+
"registry" => @registry,
|
|
38
|
+
"components" => @components,
|
|
39
|
+
}))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "uri"
|
|
7
|
+
require_relative "version"
|
|
8
|
+
|
|
9
|
+
module Wabi
|
|
10
|
+
class RegistryClient
|
|
11
|
+
DEFAULT_BASE_URL = "https://wabikit.dev/r"
|
|
12
|
+
DEFAULT_TTL = 24 * 60 * 60
|
|
13
|
+
|
|
14
|
+
attr_reader :base_url
|
|
15
|
+
|
|
16
|
+
def initialize(base_url: DEFAULT_BASE_URL)
|
|
17
|
+
@base_url = base_url
|
|
18
|
+
@ttl = Integer(ENV.fetch("WABI_CACHE_TTL", DEFAULT_TTL))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def cache_dir
|
|
22
|
+
File.expand_path("~/.cache/wabi/#{Wabi::VERSION}")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def fetch(component_name)
|
|
26
|
+
unless dev_context?
|
|
27
|
+
cached = read_cache(component_name)
|
|
28
|
+
return JSON.parse(cached) if cached
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
body =
|
|
32
|
+
if @base_url.start_with?("file://")
|
|
33
|
+
fetch_local(component_name)
|
|
34
|
+
else
|
|
35
|
+
fetch_http(component_name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
write_cache(component_name, body) unless dev_context?
|
|
39
|
+
JSON.parse(body)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def dev_context?
|
|
45
|
+
@base_url.start_with?("file://", "http://localhost", "http://127.")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def fetch_local(name)
|
|
49
|
+
path = File.join(@base_url.sub("file://", ""), "#{name}.json")
|
|
50
|
+
raise Wabi::Error, "Component #{name} not found at #{path}" unless File.exist?(path)
|
|
51
|
+
File.read(path)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def fetch_http(name)
|
|
55
|
+
uri = URI.parse("#{@base_url}/#{name}.json")
|
|
56
|
+
response = Net::HTTP.get_response(uri)
|
|
57
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
58
|
+
raise Wabi::Error, "Failed to fetch #{name}: HTTP #{response.code}"
|
|
59
|
+
end
|
|
60
|
+
response.body
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def cache_path(name)
|
|
64
|
+
File.join(cache_dir, "#{name}.json")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def read_cache(name)
|
|
68
|
+
path = cache_path(name)
|
|
69
|
+
return nil unless File.exist?(path)
|
|
70
|
+
return nil if (Time.now - File.mtime(path)) > @ttl
|
|
71
|
+
File.read(path)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def write_cache(name, body)
|
|
75
|
+
FileUtils.mkdir_p(cache_dir)
|
|
76
|
+
File.write(cache_path(name), body)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wabi
|
|
4
|
+
# Extends `Turbo::Streams::TagBuilder` with a `wabi_toast` shorthand that
|
|
5
|
+
# appends a server-rendered Toast component into the singleton Toaster
|
|
6
|
+
# container.
|
|
7
|
+
#
|
|
8
|
+
# In a Rails controller / Turbo Stream view:
|
|
9
|
+
#
|
|
10
|
+
# render turbo_stream: turbo_stream.wabi_toast(
|
|
11
|
+
# title: "Saved",
|
|
12
|
+
# description: "Profile updated.",
|
|
13
|
+
# appearance: :success,
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
# Equivalent to:
|
|
17
|
+
#
|
|
18
|
+
# render turbo_stream: turbo_stream.append(
|
|
19
|
+
# "wabi-toaster",
|
|
20
|
+
# Components::UI::Toast.new(title: "Saved", description: "...", appearance: :success),
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
# The user app must have run `bin/rails g wabi:add toast` first so that
|
|
24
|
+
# `Components::UI::Toast` is defined.
|
|
25
|
+
#
|
|
26
|
+
# NOTE: this module is named `Wabi::TurboStreamExtensions` (NOT
|
|
27
|
+
# `Wabi::Rails::TurboStreamExtensions`) because nesting a `Rails` module
|
|
28
|
+
# inside `Wabi` shadows the top-level `Rails` constant from generators that
|
|
29
|
+
# reference `Rails::Generators::Base`, breaking gem load order.
|
|
30
|
+
module TurboStreamExtensions
|
|
31
|
+
def wabi_toast(toaster_id: "wabi-toaster", **toast_options)
|
|
32
|
+
toast_class = wabi_resolve_toast_class
|
|
33
|
+
append(toaster_id, toast_class.new(**toast_options))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def wabi_resolve_toast_class
|
|
39
|
+
Object.const_get("Components::UI::Toast")
|
|
40
|
+
rescue NameError
|
|
41
|
+
raise NameError,
|
|
42
|
+
"Components::UI::Toast is not defined. Run `bin/rails g wabi:add toast` " \
|
|
43
|
+
"to install the component before using `turbo_stream.wabi_toast(...)`."
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Inject into Turbo's tag builder when Turbo is loaded. Gated so the gem stays
|
|
49
|
+
# usable in non-Turbo Rails apps and in pure-Ruby specs.
|
|
50
|
+
if defined?(::Turbo::Streams::TagBuilder)
|
|
51
|
+
::Turbo::Streams::TagBuilder.include(Wabi::TurboStreamExtensions)
|
|
52
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wabi
|
|
4
|
+
# CVA-style variant DSL. Use by extending into a class:
|
|
5
|
+
#
|
|
6
|
+
# class Button
|
|
7
|
+
# extend Wabi::Variants
|
|
8
|
+
# variants do
|
|
9
|
+
# base "btn"
|
|
10
|
+
# variant :size, { sm: "h-8", md: "h-10" }, default: :md
|
|
11
|
+
# end
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# Button.new.tokens(size: :sm) # => "btn h-8"
|
|
15
|
+
module Variants
|
|
16
|
+
def variants(&block)
|
|
17
|
+
definition = Definition.new
|
|
18
|
+
definition.instance_eval(&block)
|
|
19
|
+
@_wabi_variants = definition
|
|
20
|
+
define_method(:tokens) do |**opts|
|
|
21
|
+
self.class.instance_variable_get(:@_wabi_variants).resolve(**opts)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Definition
|
|
26
|
+
def initialize
|
|
27
|
+
@base = ""
|
|
28
|
+
@variants = {}
|
|
29
|
+
@defaults = {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def base(str)
|
|
33
|
+
@base = str
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def variant(key, options, default: nil)
|
|
37
|
+
@variants[key] = options
|
|
38
|
+
@defaults[key] = default if default
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def resolve(**opts)
|
|
42
|
+
parts = [@base]
|
|
43
|
+
@variants.each do |key, options|
|
|
44
|
+
selected = opts[key] || @defaults[key]
|
|
45
|
+
parts << options[selected] if selected && options[selected]
|
|
46
|
+
end
|
|
47
|
+
parts.compact.reject(&:empty?).join(" ")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/wabi/version.rb
ADDED
data/lib/wabi.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "wabi/version"
|
|
4
|
+
require_relative "wabi/base"
|
|
5
|
+
require_relative "wabi/class_merge"
|
|
6
|
+
require_relative "wabi/variants"
|
|
7
|
+
require_relative "wabi/registry_client"
|
|
8
|
+
require_relative "wabi/lockfile"
|
|
9
|
+
require_relative "wabi/turbo_stream_extensions"
|
|
10
|
+
|
|
11
|
+
module Wabi
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Generators are only loaded under Rails (which triggers them via convention)
|
|
16
|
+
if defined?(Rails::Generators)
|
|
17
|
+
require_relative "wabi/generators/install_generator"
|
|
18
|
+
require_relative "wabi/generators/add_generator"
|
|
19
|
+
require_relative "wabi/generators/list_generator"
|
|
20
|
+
require_relative "wabi/generators/registry_generator"
|
|
21
|
+
require_relative "wabi/generators/theme_generator"
|
|
22
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Wabi theme controller — toggles data-mode on <html>, persists to localStorage,
|
|
4
|
+
// respects prefers-color-scheme on first load.
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static values = {
|
|
7
|
+
themeKey: { type: String, default: "wabi-theme" },
|
|
8
|
+
modeKey: { type: String, default: "wabi-mode" },
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
connect() {
|
|
12
|
+
const html = document.documentElement
|
|
13
|
+
const storedTheme = localStorage.getItem(this.themeKeyValue) || "default"
|
|
14
|
+
const storedMode = localStorage.getItem(this.modeKeyValue) || this.systemMode()
|
|
15
|
+
html.dataset.theme = storedTheme
|
|
16
|
+
html.dataset.mode = storedMode
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
toggleMode() {
|
|
20
|
+
const html = document.documentElement
|
|
21
|
+
const next = html.dataset.mode === "dark" ? "light" : "dark"
|
|
22
|
+
html.dataset.mode = next
|
|
23
|
+
localStorage.setItem(this.modeKeyValue, next)
|
|
24
|
+
this.dispatch("change", { detail: { mode: next } })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
setTheme(event) {
|
|
28
|
+
const theme = event.params.theme
|
|
29
|
+
document.documentElement.dataset.theme = theme
|
|
30
|
+
localStorage.setItem(this.themeKeyValue, theme)
|
|
31
|
+
this.dispatch("change", { detail: { theme } })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
systemMode() {
|
|
35
|
+
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/* Wabi theme system — Tailwind 4 native, shared infrastructure.
|
|
2
|
+
*
|
|
3
|
+
* Every Wabi theme CSS file is the concatenation of this `_shared.css` and a
|
|
4
|
+
* per-theme `<slug>.css` file containing only the raw HSL variable blocks.
|
|
5
|
+
* The build step in `registry/lib/builder.rb` emits the merged result to
|
|
6
|
+
* `gem/templates/tokens.css` (for the gem's wabi:install) and
|
|
7
|
+
* `docs/app/assets/tailwind/wabi/tokens.css` (for live switching in docs).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
@custom-variant dark (&:where([data-mode="dark"], [data-mode="dark"] *));
|
|
11
|
+
|
|
12
|
+
@theme inline {
|
|
13
|
+
--color-background: hsl(var(--background));
|
|
14
|
+
--color-foreground: hsl(var(--foreground));
|
|
15
|
+
--color-card: hsl(var(--card));
|
|
16
|
+
--color-card-foreground: hsl(var(--card-foreground));
|
|
17
|
+
--color-popover: hsl(var(--popover));
|
|
18
|
+
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
19
|
+
--color-primary: hsl(var(--primary));
|
|
20
|
+
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
21
|
+
--color-secondary: hsl(var(--secondary));
|
|
22
|
+
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
23
|
+
--color-muted: hsl(var(--muted));
|
|
24
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
25
|
+
--color-accent: hsl(var(--accent));
|
|
26
|
+
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
27
|
+
--color-destructive: hsl(var(--destructive));
|
|
28
|
+
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
29
|
+
--color-border: hsl(var(--border));
|
|
30
|
+
--color-input: hsl(var(--input));
|
|
31
|
+
--color-ring: hsl(var(--ring));
|
|
32
|
+
--radius-lg: var(--radius);
|
|
33
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
34
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Wabi default theme — neutral grayscale.
|
|
38
|
+
*
|
|
39
|
+
* Raw HSL values only. The `_shared.css` companion ships the @custom-variant
|
|
40
|
+
* + @theme inline mapping; both files must be concatenated to produce a
|
|
41
|
+
* working tokens.css.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
:root, [data-theme="default"] {
|
|
45
|
+
--background: 0 0% 100%;
|
|
46
|
+
--foreground: 240 10% 3.9%;
|
|
47
|
+
--primary: 240 5.9% 10%;
|
|
48
|
+
--primary-foreground: 0 0% 98%;
|
|
49
|
+
--secondary: 240 4.8% 95.9%;
|
|
50
|
+
--secondary-foreground: 240 5.9% 10%;
|
|
51
|
+
--muted: 240 4.8% 95.9%;
|
|
52
|
+
--muted-foreground: 240 3.8% 46.1%;
|
|
53
|
+
--accent: 240 4.8% 95.9%;
|
|
54
|
+
--accent-foreground: 240 5.9% 10%;
|
|
55
|
+
--destructive: 0 84.2% 60.2%;
|
|
56
|
+
--destructive-foreground: 0 0% 98%;
|
|
57
|
+
--border: 240 5.9% 90%;
|
|
58
|
+
--input: 240 5.9% 90%;
|
|
59
|
+
--ring: 240 5.9% 10%;
|
|
60
|
+
--radius: 0.5rem;
|
|
61
|
+
--card: 0 0% 100%;
|
|
62
|
+
--card-foreground: 240 10% 3.9%;
|
|
63
|
+
--popover: 0 0% 100%;
|
|
64
|
+
--popover-foreground: 240 10% 3.9%;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[data-theme="default"][data-mode="dark"] {
|
|
68
|
+
--background: 240 10% 3.9%;
|
|
69
|
+
--foreground: 0 0% 98%;
|
|
70
|
+
--primary: 0 0% 98%;
|
|
71
|
+
--primary-foreground: 240 5.9% 10%;
|
|
72
|
+
--secondary: 240 3.7% 15.9%;
|
|
73
|
+
--secondary-foreground: 0 0% 98%;
|
|
74
|
+
--muted: 240 3.7% 15.9%;
|
|
75
|
+
--muted-foreground: 240 5% 64.9%;
|
|
76
|
+
--accent: 240 3.7% 15.9%;
|
|
77
|
+
--accent-foreground: 0 0% 98%;
|
|
78
|
+
--destructive: 0 62.8% 30.6%;
|
|
79
|
+
--destructive-foreground: 0 0% 98%;
|
|
80
|
+
--border: 240 3.7% 15.9%;
|
|
81
|
+
--input: 240 3.7% 15.9%;
|
|
82
|
+
--ring: 240 4.9% 83.9%;
|
|
83
|
+
--card: 240 10% 3.9%;
|
|
84
|
+
--card-foreground: 0 0% 98%;
|
|
85
|
+
--popover: 240 10% 3.9%;
|
|
86
|
+
--popover-foreground: 0 0% 98%;
|
|
87
|
+
}
|
metadata
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: wabi
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Oscar Ortega
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: phlex-rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.4'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.4'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '8.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '8.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: zeitwerk
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.8'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.8'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.13'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.13'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: fakefs
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.2'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.2'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: webmock
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.26'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '3.26'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rake
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '13.0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '13.0'
|
|
110
|
+
description: An OSS UI component library for Rails 8 — Phlex-native, Tailwind-themed,
|
|
111
|
+
accessible, with 'you own the code' philosophy.
|
|
112
|
+
email:
|
|
113
|
+
- dev@cosmaneura.com
|
|
114
|
+
executables: []
|
|
115
|
+
extensions: []
|
|
116
|
+
extra_rdoc_files: []
|
|
117
|
+
files:
|
|
118
|
+
- CHANGELOG.md
|
|
119
|
+
- LICENSE
|
|
120
|
+
- README.md
|
|
121
|
+
- lib/wabi.rb
|
|
122
|
+
- lib/wabi/base.rb
|
|
123
|
+
- lib/wabi/class_merge.rb
|
|
124
|
+
- lib/wabi/generators/add_generator.rb
|
|
125
|
+
- lib/wabi/generators/install_generator.rb
|
|
126
|
+
- lib/wabi/generators/list_generator.rb
|
|
127
|
+
- lib/wabi/generators/registry_generator.rb
|
|
128
|
+
- lib/wabi/generators/theme_generator.rb
|
|
129
|
+
- lib/wabi/lockfile.rb
|
|
130
|
+
- lib/wabi/registry_client.rb
|
|
131
|
+
- lib/wabi/turbo_stream_extensions.rb
|
|
132
|
+
- lib/wabi/variants.rb
|
|
133
|
+
- lib/wabi/version.rb
|
|
134
|
+
- templates/controllers/wabi/theme_controller.js
|
|
135
|
+
- templates/tokens.css
|
|
136
|
+
homepage: https://wabikit.dev
|
|
137
|
+
licenses:
|
|
138
|
+
- MIT
|
|
139
|
+
metadata:
|
|
140
|
+
homepage_uri: https://wabikit.dev
|
|
141
|
+
source_code_uri: https://github.com/wabikit/wabi
|
|
142
|
+
bug_tracker_uri: https://github.com/wabikit/wabi/issues
|
|
143
|
+
changelog_uri: https://github.com/wabikit/wabi/blob/main/CHANGELOG.md
|
|
144
|
+
rdoc_options: []
|
|
145
|
+
require_paths:
|
|
146
|
+
- lib
|
|
147
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - ">="
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '3.4'
|
|
152
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
153
|
+
requirements:
|
|
154
|
+
- - ">="
|
|
155
|
+
- !ruby/object:Gem::Version
|
|
156
|
+
version: '0'
|
|
157
|
+
requirements: []
|
|
158
|
+
rubygems_version: 4.0.10
|
|
159
|
+
specification_version: 4
|
|
160
|
+
summary: Beautifully imperfect components for Rails.
|
|
161
|
+
test_files: []
|