@evinvest/uikit 0.29.4 → 0.30.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.
package/README.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # @evinvest/uikit
2
2
 
3
3
  A **dep-light** React UI kit with shadcn-style semantics — the TypeScript mirror
4
- of the `uikit` feature of the [`ev_lib`](https://github.com/EV-invest/lib) Rust crate
5
- (`ev_lib::uikit`, Dioxus). Both ports are driven from the same canonical class
6
- strings and the same design tokens, so a component looks and behaves the same
7
- whether it is rendered by React or by Dioxus.
4
+ of the UI kit of the [`ev_lib`](https://github.com/EV-invest/lib) Rust crate,
5
+ which ships it for Dioxus (`ev_lib::uikit_dioxus`) and Leptos
6
+ (`ev_lib::uikit_leptos`) over one framework-free core. All three are driven from
7
+ the same canonical class strings and the same design tokens, so a component
8
+ looks and behaves the same whether it is rendered by React, Dioxus or Leptos.
8
9
 
9
10
  "Dep-light" means: **no `@radix-ui/*`, no `class-variance-authority`, no
10
11
  `lucide-react`, no charting / carousel / date-picker / OTP / toast libraries.**
@@ -584,9 +585,11 @@ hold focus, so a bare list is pointer-only. The live demo is `CommandDemo` in
584
585
 
585
586
  The Rust crate is the source of truth; this package preserves its _semantics_
586
587
  while reading like idiomatic React. Canonical class strings are identical per
587
- element across the two ports.
588
+ element across all ports. The two Rust ports render the same HTML (pinned by
589
+ `rust/tests/parity.rs`) and share every row below; where Leptos idiom differs
590
+ the cell says so.
588
591
 
589
- | Concept | Rust (`ev_lib::uikit`) | TS (`@evinvest/uikit`) |
592
+ | Concept | Rust (`ev_lib::uikit_dioxus` / `ev_lib::uikit_leptos`) | TS (`@evinvest/uikit`) |
590
593
  | --- | --- | --- |
591
594
  | class fusion | `cn!` macro (`tailwind_fuse`) | `cn` (`clsx` + `tailwind-merge`) |
592
595
  | variants | `enum` + `fn class()` match | `as const` map keyed by variant |
@@ -595,6 +598,10 @@ element across the two ports.
595
598
  | keyboard nav | `use_roving_focus` | `useRovingFocus` |
596
599
  | overlay placement | inline `position:fixed` + `data-side` | `Portal` + `useFloating` |
597
600
  | dismiss / focus trap | full-screen backdrop / native order | `useDismissableLayer` / `useFocusScope` |
601
+ | brand webfonts | `Fonts`: Dioxus bundles the faces through `asset!`; Leptos has no asset pipeline, so `Fonts base="/fonts"` points the same `@font-face` rules at `base` and the consumer serves `FONT_FILES` there | none — load the files yourself ([Fonts](#fonts)) |
602
+ | optional slot present | an explicit prop, never read off `children`: `FieldSeparator { content: true, "or" }` renders the copy pill; `FormMessage { error, .. }` renders only while `error` (the same flag as `FormControl`'s) | `!!children` / `if (!children) return null` |
603
+ | option label | `SelectItem { value, text_value, .. }`: the text type-ahead matches, an explicit prop in both ports — never read off `children` | `SelectItem`'s `textValue`, else the text of its children |
604
+ | toasts | `ToasterProvider` + `use_toaster()` → `ToasterHandle` (`.toast` `.positive` `.error` `.info` `.warn` `.show` `.dismiss`), same in both ports | global `toast(...)` / `toast.dismiss` |
598
605
 
599
606
  ### Component inventory (all 75 bricks)
600
607
 
@@ -668,8 +675,10 @@ measuring needs host-only `web-sys`). Known gaps:
668
675
  (`modal`, `pointerOutside`) and `DismissableLayerScope`; mark a floating
669
676
  region that should dismiss nothing with `LAYER_IGNORE_ATTR`.
670
677
  - **overlay layers (Rust):** `Select`, `Popover`, `Dialog` and `DropdownMenu`
671
- share one dismissable-layer stack per `VirtualDom`
672
- (`primitives::use_dismissable_layer`): Escape and a click on a backdrop act
678
+ share one dismissable-layer stack per `VirtualDom` in Dioxus, per root owner
679
+ (each island its own) in Leptos (`primitives::use_dismissable_layer`). The
680
+ stack follows renders, not handlers: a layer that closes itself stays on top
681
+ until it re-renders closed. Escape and a click on a backdrop act
673
682
  on the top layer only, so a `Select` inside a `Dialog` closes first. There is
674
683
  no document listener — an Escape counts only once it reaches an open layer's
675
684
  `onkeydown` — and `AlertDialog`, `Sheet`, `Drawer`, `ContextMenu`, `Menubar`,
@@ -707,19 +716,19 @@ measuring needs host-only `web-sys`). Known gaps:
707
716
  focus, via the shared `data-stack` CSS in `tokens.css`. The enter is a CSS
708
717
  keyframe (plays on insertion), the exit slides out on `data-state="closed"` and
709
718
  unmounts on `transitionend` (no host timer). React measures each toast's height
710
- for exact expanded spacing; Dioxus can't (layout measuring needs host-only
711
- `web-sys`), so it assumes a constant height — collapsed pile exact, expanded
712
- list uniform. Both **auto-dismiss** (default 4000ms) and **pause while the stack
719
+ for exact expanded spacing; Rust measures nothing, so it assumes a constant
720
+ height — collapsed pile exact, expanded list uniform. Both **auto-dismiss** (default 4000ms) and **pause while the stack
713
721
  is hovered/focused**, and support **persistent** toasts; React does it with a
714
- `setTimeout` (`duration: Infinity` to persist), Dioxus with a host-timer-free
722
+ `setTimeout` (`duration: Infinity` to persist), both Rust ports with a host-timer-free
715
723
  CSS "life" animation whose `animationend` closes the toast (`use_toaster().show(
716
724
  msg, variant, None)` to persist). Swipe-to-dismiss (horizontal drag, past 45px
717
725
  or a fast flick) is TS-only pointer physics.
718
726
  - **form:** react-hook-form is dropped — these are presentational + ARIA-id
719
727
  wiring; consumers own validation/state. Rust `FormControl` can't inject ids
720
728
  onto an arbitrary child (no `Slot`), so it publishes them as a
721
- `FormControlContext` that `Input`/`Textarea` consume; wrap a bare element and
722
- you wire them yourself.
729
+ `FormControlContext` signal that `Input`/`Textarea` consume; wrap a bare
730
+ element and you wire them yourself. Rust `FormMessage` renders on its `error`
731
+ flag, not on having children.
723
732
  - **carousel:** pointer-drag physics are TS-only (prev/next + keyboard in
724
733
  Rust). Embla momentum is not reproduced.
725
734
  - **drawer:** the Vaul-style enter/exit motion is shared by both ports via
@@ -769,7 +778,8 @@ npm run build # tsup → dist/ (ESM + d.ts)
769
778
  The Rust counterpart is verified from the repo root:
770
779
 
771
780
  ```sh
772
- cargo test -p ev_lib --features uikit
773
- cargo clippy -p ev_lib --features uikit --all-targets -- -D warnings
774
- cargo check -p ev_lib --features "uikit wasm" --target wasm32-unknown-unknown
781
+ cargo test -p ev_lib --features "uikit_dioxus uikit_leptos"
782
+ cargo clippy -p ev_lib --features "uikit_dioxus uikit_leptos" --all-targets -- -D warnings
783
+ cargo check -p ev_lib --features "uikit_dioxus wasm" --target wasm32-unknown-unknown
784
+ cargo check -p ev_lib --features "uikit_leptos wasm" --target wasm32-unknown-unknown
775
785
  ```
@@ -2,7 +2,7 @@ const COMMAND_ROOT = "bg-popover text-ink flex h-full w-full flex-col overflow-h
2
2
  const COMMAND_DIALOG_OVERLAY = "fixed inset-0 z-50 bg-black/50";
3
3
  const COMMAND_DIALOG_CONTENT = "fixed top-1/2 left-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-lg border border-border p-0 shadow-lg";
4
4
  const COMMAND_DIALOG_COMMAND = "[&_[data-slot=command-input-wrapper]]:h-12 [&_[data-slot=command-input]]:h-12";
5
- const COMMAND_INPUT_WRAPPER = "flex h-9 items-center gap-2 border-b border-border px-3";
5
+ const COMMAND_INPUT_WRAPPER = "flex h-9 items-center gap-2 border-b border-border px-3 has-[[aria-invalid=true]]:border-accent-error";
6
6
  const COMMAND_INPUT = "placeholder:text-ink-soft flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50";
7
7
  const COMMAND_LIST = "grid max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto";
8
8
  const COMMAND_EMPTY = "py-6 text-center text-sm";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/generated/command.ts"],"sourcesContent":["// AUTO-GENERATED by `nix run .#gen` — do not edit.\n\nexport const COMMAND_ROOT = \"bg-popover text-ink flex h-full w-full flex-col overflow-hidden rounded-md\";\n\nexport const COMMAND_DIALOG_OVERLAY = \"fixed inset-0 z-50 bg-black/50\";\n\nexport const COMMAND_DIALOG_CONTENT = \"fixed top-1/2 left-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-lg border border-border p-0 shadow-lg\";\n\nexport const COMMAND_DIALOG_COMMAND = \"[&_[data-slot=command-input-wrapper]]:h-12 [&_[data-slot=command-input]]:h-12\";\n\nexport const COMMAND_INPUT_WRAPPER = \"flex h-9 items-center gap-2 border-b border-border px-3\";\n\nexport const COMMAND_INPUT = \"placeholder:text-ink-soft flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50\";\n\nexport const COMMAND_LIST = \"grid max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto\";\n\nexport const COMMAND_EMPTY = \"py-6 text-center text-sm\";\n\nexport const COMMAND_GROUP = \"grid text-ink [&_[data-slot=command-group-heading]]:text-ink-soft overflow-hidden p-1 [&>[data-slot=command-group-heading]]:order-first [&_[data-slot=command-group-heading]]:px-2 [&_[data-slot=command-group-heading]]:py-1.5 [&_[data-slot=command-group-heading]]:text-xs [&_[data-slot=command-group-heading]]:font-medium\";\n\nexport const COMMAND_ITEM = \"data-[selected=true]:bg-hover data-[selected=true]:text-ink [&_svg:not([class*='text-'])]:text-ink-soft relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\";\n\nexport const COMMAND_SEPARATOR = \"bg-border -mx-1 h-px\";\n\nexport const COMMAND_SHORTCUT = \"text-ink-soft ml-auto text-xs tracking-widest\";\n"],"mappings":"AAEO,MAAM,eAAe;AAErB,MAAM,yBAAyB;AAE/B,MAAM,yBAAyB;AAE/B,MAAM,yBAAyB;AAE/B,MAAM,wBAAwB;AAE9B,MAAM,gBAAgB;AAEtB,MAAM,eAAe;AAErB,MAAM,gBAAgB;AAEtB,MAAM,gBAAgB;AAEtB,MAAM,eAAe;AAErB,MAAM,oBAAoB;AAE1B,MAAM,mBAAmB;","names":[]}
1
+ {"version":3,"sources":["../../src/generated/command.ts"],"sourcesContent":["// AUTO-GENERATED by `nix run .#gen` — do not edit.\n\nexport const COMMAND_ROOT = \"bg-popover text-ink flex h-full w-full flex-col overflow-hidden rounded-md\";\n\nexport const COMMAND_DIALOG_OVERLAY = \"fixed inset-0 z-50 bg-black/50\";\n\nexport const COMMAND_DIALOG_CONTENT = \"fixed top-1/2 left-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-lg border border-border p-0 shadow-lg\";\n\nexport const COMMAND_DIALOG_COMMAND = \"[&_[data-slot=command-input-wrapper]]:h-12 [&_[data-slot=command-input]]:h-12\";\n\nexport const COMMAND_INPUT_WRAPPER = \"flex h-9 items-center gap-2 border-b border-border px-3 has-[[aria-invalid=true]]:border-accent-error\";\n\nexport const COMMAND_INPUT = \"placeholder:text-ink-soft flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50\";\n\nexport const COMMAND_LIST = \"grid max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto\";\n\nexport const COMMAND_EMPTY = \"py-6 text-center text-sm\";\n\nexport const COMMAND_GROUP = \"grid text-ink [&_[data-slot=command-group-heading]]:text-ink-soft overflow-hidden p-1 [&>[data-slot=command-group-heading]]:order-first [&_[data-slot=command-group-heading]]:px-2 [&_[data-slot=command-group-heading]]:py-1.5 [&_[data-slot=command-group-heading]]:text-xs [&_[data-slot=command-group-heading]]:font-medium\";\n\nexport const COMMAND_ITEM = \"data-[selected=true]:bg-hover data-[selected=true]:text-ink [&_svg:not([class*='text-'])]:text-ink-soft relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\";\n\nexport const COMMAND_SEPARATOR = \"bg-border -mx-1 h-px\";\n\nexport const COMMAND_SHORTCUT = \"text-ink-soft ml-auto text-xs tracking-widest\";\n"],"mappings":"AAEO,MAAM,eAAe;AAErB,MAAM,yBAAyB;AAE/B,MAAM,yBAAyB;AAE/B,MAAM,yBAAyB;AAE/B,MAAM,wBAAwB;AAE9B,MAAM,gBAAgB;AAEtB,MAAM,eAAe;AAErB,MAAM,gBAAgB;AAEtB,MAAM,gBAAgB;AAEtB,MAAM,eAAe;AAErB,MAAM,oBAAoB;AAE1B,MAAM,mBAAmB;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evinvest/uikit",
3
- "version": "0.29.4",
3
+ "version": "0.30.0",
4
4
  "description": "Dep-light React UI kit: shadcn-semantics components with zero @radix-ui / cva, mirroring the ev_lib::uikit Rust feature. Ships the shared design tokens.",
5
5
  "license": "BlueOak-1.0.0",
6
6
  "publishConfig": {