@revenyu/ui
v0.15.1
Published
Shared Vue 3 components, app-shell, and Frappe API layer for Revenyu-suite apps (HR Recruiter, Payroll, CRM, ERP).
Maintainers
Readme
@revenyu/ui
Shared Vue 3 components, app-shell chrome, and Frappe API layer extracted from hr_recruiter,
for reuse across Revenyu-suite apps (HR Recruiter, Payroll, CRM, ERP — see the logos in
AppSwitcher). Ships raw .vue/.ts source, same as frappe-ui: consumers compile it with
their own Vite/tsc, there is no prebuilt dist.
This package assumes a Frappe backend. It is not a framework/backend-agnostic UI kit —
api/* calls whitelisted Frappe methods, lib/doppio/controllers/call.js talks to
/api/method/* with Frappe's CSRF header, and several components read/write Frappe doctypes.
Install
Published to the public npm registry as @revenyu/ui. The GitHub repo
(PROJECXIO/revenyu-ui) is private, but the npm
package is not — installing needs no GitHub access and no auth token:
yarn add @revenyu/ui// package.json
"dependencies": {
"@revenyu/ui": "^0.15.0"
}No build step runs on install — your own Vite/tsc compiles the .vue/.ts source directly out
of node_modules/@revenyu/ui/src/..., same as frappe-ui.
Peer dependencies you must already have: vue >=3.5, vue-router >=4.
Pin
>=0.15.0if you useLinkSelect.0.14.0was published from the commit beforeLinkSelectlanded, so that one version's tarball has no such export and consuming builds fail with[MISSING_EXPORT] "LinkSelect" is not exported by ".../@revenyu/ui/src/index.ts".
Package entrypoints
Only these five subpaths are importable — the exports map deliberately exposes no others, so
api/*, utils/*, and composables/useAnchoredPosition are internal to the library and not
part of the public surface:
| Import | What it is |
| -------------------------------- | -------------------------------------------------------- |
| @revenyu/ui | Every component, store, composable and type listed below |
| @revenyu/ui/style.css | The design tokens every component reads via var(--x) |
| @revenyu/ui/rv-list.css | The opt-in .rv-* list-page stylesheet |
| @revenyu/ui/tailwind | Tailwind preset mapping the tokens to theme values |
| @revenyu/ui/tsconfig.base.json | Shared strict TS compiler options |
Setup
// main.ts
import '@revenyu/ui/style.css'
import { setCallRouter } from '@revenyu/ui'
import router from './router'
setCallRouter(router) // lets the API layer redirect to your login route on 401/403Your router needs a route named Login (used by the 401/403 redirect check).
Mount these once at your app root — they are singletons driven by the exported stores, not per-instance props/emits components:
<SelectSheet />
<DateSheet />
<ConfirmSheet />
<Toasts />Optional Tailwind preset (maps the CSS-variable design tokens to Tailwind theme values):
// tailwind.config.js
import revenyuPreset from '@revenyu/ui/tailwind'
export default { presets: [revenyuPreset] }What's in here
Every name below is exported from the package root:
import { Badge, SideNav, AiRewriteSheet } from '@revenyu/ui'.
Primitives — form inputs
Button, Checkbox, Combobox, CustomSelect, DatePickerField, FormControl,
FormLabel, IconButton, Password, Radio, Slider, Switch, TextInput, Textarea,
TimePicker.
Primitives — overlays/floating
BottomSheet, ConfirmSheet, ContextMenu, DateSheet, Dialog, Dropdown,
FileUploader, HoverCard, Popover, SelectSheet, SettingsDialog, Tooltip.
Primitives — feedback/loading
Alert, ErrorMessage, LoadingIndicator, LoadingText, Progress, Skeleton, Spinner,
Toasts.
Primitives — layout/navigation
ActionBar, Breadcrumb, BreadcrumbChip, Breadcrumbs, CrmBreadcrumb, Divider,
EmptyState, ItemListRow, ListToolbar, ListFilterFields, ListView, PageHeader,
RangeSwitcher, ScrollArea, Section, SettingsMenu, Stepper, TabButtons, Tabs,
Tree.
ListToolbar + ListFilterFields + ListView + BulkActionBar together are the "Revenyu
list page" pattern — search, a responsive filter grid, a sortable/selectable table, column
editing, pagination and a bulk-action bar. They render against @revenyu/ui/rv-list.css.
Primitives — misc
Avatar, Badge, Duration, EChart, Icon, KeyboardShortcut, KeyboardShortcutsDialog,
KpiCard, Rating, RevenyuLogo, RevenyuUiProvider.
App shell
AppSwitcher, BottomNav, MoreDrawer, Notifications, PageCrumbs, ProfileMenu,
Shell, ShellDesktop, Sidebar, SideNav, Topbar, TopbarDesktop, GlobalSearch.
Feature widgets
ActivityFeed, BulkActionBar, DeleteAction, DocViewers, EmailComposer,
FieldValueInput, InterviewQuickCreate, LinkSelect, OnboardingFieldCard,
OnboardingLauncher, TableColumnsEditor, TemplateEditor.
LinkSelect is a Link field's picker: everything CustomSelect does, plus a create action in
the dropdown header, offered only to users the server says may create that doctype. The record
it creates is selected and kept alongside the options the parent passed, so the surrounding
form needs to know nothing about a record being created mid-edit.
AI wizards
AiActionButton, AiDescriptionWizard, AiOnboardingFieldsWizard, AiQuestionsWizard,
AiRewriteSheet, AiSettingsSheet.
AiEmailTemplateWizard exists in the source but is intentionally not exported — its
template only renders a permanent "Coming Soon" placeholder. See CHANGELOG.md.
Easily-confused pairs
Three pairs of similarly-named components are easy to mix up — pick based on this:
Breadcrumb(fixed parent→current pair) vsBreadcrumbs(an arbitrary-length, prop-driven trail) vsCrmBreadcrumb(a segmented chip trail, each segment clickable).RangeSwitcher(hardcoded 7d/30d/90d/ytd/all date-range options) vsTabButtons(a generic segmented control taking anyoptionsarray).PageCrumbs(breadcrumb trail + subtitle, driven by this app's own route names) vsPageHeader(a generic title + subtitle + actions-slot header, unrelated to routing).
Imperative UI helpers
The singleton sheets are driven by function calls rather than props, so any code — including non-component code — can open them:
import { openSelect, openDate, askConfirm, askConfirmWithReason, notify } from '@revenyu/ui'
openSelect('Status', ['Open', 'Closed'], current, v => (current = v))
openDate(dateStr, v => (dateStr = v), { min: '2026-01-01', quickActions: true })
const ok = await askConfirm({ title: 'Delete job?', message: 'This cannot be undone.' })
const reason = await askConfirmWithReason({ title: 'Reject applicant?' }) // null if cancelled
notify('Job deleted', 'ok')| Function | Signature / notes |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| openSelect | (label, options, value, onChange, opts?) — options accepts plain strings or { value, label }; opts takes { createLabel, onCreate } to offer the same create action CustomSelect has on desktop |
| openDate | (value, onChange, opts?) — opts takes { min, quickActions, anchor } |
| askConfirm | (ConfirmOpts) => Promise<boolean> |
| askConfirmWithReason | (ConfirmOpts) => Promise<string \| null> — resolves to the typed reason, or null when cancelled |
| closeAll | Closes the select/date/confirm sheets at once |
| notify | (text, kind?, ms?, title?) — kind is 'ok' \| 'err' (default 'ok'), ms defaults to 2200 for 'ok' / 6000 for 'err', title is an optional bold heading (used by error toasts to name the failure kind) |
| toggleSidebar | Flips ui.sidebarCollapsed, persists it to localStorage, and sets the --sidebar-w custom property (68px collapsed / 220px expanded) |
ConfirmOpts is { title, message?, confirmLabel?, cancelLabel?, danger?, icon?,
reasonRequired?, reasonPlaceholder? }.
Stores
Plain Vue reactive() objects — no Pinia, nothing to install or register.
ui— the shared chrome state: theselect/date/confirmsheets plus the open/closed flags every shell component reads (notifs,appSwitcher,profileMenu,more,search,aiSettings,aiDescriptionWizard,aiRewrite,aiEmailWizard,aiQuestionsWizard,sidebarCollapsed).toasts—{ items }, rendered by<Toasts />; push to it withnotify().userStore—{ profile, loading, loaded }, withloadUser(force?),userInitial()anduserImage()(which normalizes a Frappeuser_imageinto a usable URL).- Notification count —
unreadCount, plusrefreshUnread(),setUnread(n),incUnread(by?)anddecUnread(by?)so a screen that marks something read can correct the bell without a refetch.
Composables
useIsDesktop()— aref<boolean>, true atwindow.innerWidth >= 900, initialised synchronously so the first render is already correct and kept current on resize.useDialogLayer(step = 10)— returns a dialog'sz-indexand hands the next one down a layer. The depth travels down the component tree rather than through a module-level counter, so nested dialogs (a mandatory Link field inside a quick-create dialog opening another quick-create dialog) are ordered right by construction and closing one leaves nothing to unwind.MODAL_LAYER(2000) is the base; toasts stay above at 12000.useRevenyuUiConfig()— reads the config provided byRevenyuUiProvider. Entirely optional: it falls back to built-in defaults when no provider is mounted.
Configuration
RevenyuUiProvider
Wrap your app to set library-wide defaults — toastDuration, iconSize, size. Every field
is optional and components fall back to the defaults, so mounting the provider is never
required.
configureNotifications
The shared Notifications panel and bell default to HR Recruiter's hiring-scoped backend and
routes. Any other app calls this once at startup; partial overrides are merged over the
default, so you can replace just routeFor and keep the rest:
import { configureNotifications } from '@revenyu/ui'
configureNotifications({
list: limit => call('myapp.api.notifications.list', { limit }),
routeFor: n => (n.document_type === 'Lead' ? `/leads/${n.document_name}` : null),
})The full contract is NotificationsApi: list, unreadCount, markRead, markAllRead,
dismiss, routeFor. getNotificationsApi() returns whatever is currently installed.
setCallRouter
Hands the lib/doppio fetch wrapper your router so a 401/403 from any API call can redirect to
your app's own login route. Call it once at startup.
Exported types
ConfirmOpts, CrmBreadcrumbSegment, DropdownItem, ListFilterFieldDef,
ListFilterOption, ListToolbarColumn, ListToolbarSortOption, ListViewColumn,
NotificationRow, NotificationType, NotificationsApi, ProfileMenuItem,
SearchEntityStyle, SearchResultGroup, SearchResultItem, SettingsMenuItem,
StepperStep.
Design tokens and styles
src/style.css (exported as @revenyu/ui/style.css) defines the
color/radius/shadow/typography CSS custom properties every component reads via var(--x).
Import it once at your app root.
@revenyu/ui/rv-list.css is separate and opt-in: the list-page pattern (search + filter grid
- sortable/checkbox table + popovers + pagination + bulk bar) ported verbatim from
revenyu_v2's CRM Leads list, so every consuming app renders byte-identical output instead of hand-porting values per app. Its tokens are namespaced under--rv-*, so importing it never overwrites your app's own--primary,--border, and so on.
Shared TypeScript config
// tsconfig.json
{ "extends": "@revenyu/ui/tsconfig.base.json" }Gives you the same strict, moduleResolution: "Bundler", isolatedModules baseline the
library itself compiles under.
Known limitations
BottomNav, SideNav, MoreDrawer, and PageCrumbs are not yet generic — their nav
items and page titles (paths, icons, labels — see src/utils/pageMeta.ts for PageCrumbs)
are hardcoded to HR Recruiter's own routes (/jobs, /applicants, /interviews, /offers,
/onboarding, /settings/team, …), not driven by props. A consumer app other than HR
Recruiter that mounts Shell/ShellDesktop today will get HR Recruiter's nav and breadcrumbs
pointing at routes it doesn't have. Treat these four as HR-Recruiter-only until they're made
configurable (tracked in CHANGELOG.md).
Topbar, TopbarDesktop, AppSwitcher, Notifications, ProfileMenu, and GlobalSearch
are not affected — they read their state from the shared stores, not from hardcoded nav/route
data. Breadcrumb, Breadcrumbs, and CrmBreadcrumb take their items via props.
Component docs
yarn docs:dev runs a local VitePress site with a live, adjustable-prop playground for every
component, plus a full composed page example. Not deployed
anywhere yet — clone this repo and run it locally.
Development
yarn install
yarn lint # eslint
yarn format:check # prettier
yarn type-check # vue-tsc --noEmit
yarn test # vitest
yarn pack:check # confirm `exports`/`files` resolve correctly before publishingVersioning
Semver: patch = fixes, minor = new components/props, major = breaking prop/behavior changes
(e.g. what openSelect/askConfirm accept). Consumers bump with yarn upgrade @revenyu/ui.
Releasing: bump version in package.json in the same commit or later than the feature it
ships, tag that commit vX.Y.Z, push the tag, npm publish, then cut a
GitHub release. Bumping in an earlier
commit than the feature is what produced the broken 0.14.0 described above.
