omniconn-design-system 1.11.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/CHANGELOG.md +213 -0
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/dist/components/ActionMenu.js +104 -0
- package/dist/components/ActionMenu.js.map +1 -0
- package/dist/components/Avatar.js +97 -0
- package/dist/components/Avatar.js.map +1 -0
- package/dist/components/Badge.js +37 -0
- package/dist/components/Badge.js.map +1 -0
- package/dist/components/Button.js +80 -0
- package/dist/components/Button.js.map +1 -0
- package/dist/components/Card.js +122 -0
- package/dist/components/Card.js.map +1 -0
- package/dist/components/Checkbox.js +68 -0
- package/dist/components/Checkbox.js.map +1 -0
- package/dist/components/DatePicker.js +624 -0
- package/dist/components/DatePicker.js.map +1 -0
- package/dist/components/Drawer.js +227 -0
- package/dist/components/Drawer.js.map +1 -0
- package/dist/components/Field.js +29 -0
- package/dist/components/Field.js.map +1 -0
- package/dist/components/Input.js +32 -0
- package/dist/components/Input.js.map +1 -0
- package/dist/components/Menu.js +293 -0
- package/dist/components/Menu.js.map +1 -0
- package/dist/components/Modal.js +195 -0
- package/dist/components/Modal.js.map +1 -0
- package/dist/components/PageHeader.js +207 -0
- package/dist/components/PageHeader.js.map +1 -0
- package/dist/components/Pagination.js +143 -0
- package/dist/components/Pagination.js.map +1 -0
- package/dist/components/RadioGroup.js +114 -0
- package/dist/components/RadioGroup.js.map +1 -0
- package/dist/components/SearchBar.js +167 -0
- package/dist/components/SearchBar.js.map +1 -0
- package/dist/components/SegmentedToggle.js +56 -0
- package/dist/components/SegmentedToggle.js.map +1 -0
- package/dist/components/Select.js +338 -0
- package/dist/components/Select.js.map +1 -0
- package/dist/components/Switch.js +67 -0
- package/dist/components/Switch.js.map +1 -0
- package/dist/components/Table.js +586 -0
- package/dist/components/Table.js.map +1 -0
- package/dist/components/Tabs.js +68 -0
- package/dist/components/Tabs.js.map +1 -0
- package/dist/components/Textarea.js +62 -0
- package/dist/components/Textarea.js.map +1 -0
- package/dist/i18n/DesignSystemProvider.js +23 -0
- package/dist/i18n/DesignSystemProvider.js.map +1 -0
- package/dist/i18n/strings.ar.js +82 -0
- package/dist/i18n/strings.ar.js.map +1 -0
- package/dist/i18n/strings.en.js +82 -0
- package/dist/i18n/strings.en.js.map +1 -0
- package/dist/i18n/translator.js +82 -0
- package/dist/i18n/translator.js.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/actionVisibility.js +19 -0
- package/dist/lib/actionVisibility.js.map +1 -0
- package/dist/lib/breakpoints.js +69 -0
- package/dist/lib/breakpoints.js.map +1 -0
- package/dist/lib/cn.js +7 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/lib/tone.js +186 -0
- package/dist/lib/tone.js.map +1 -0
- package/dist/lib/useBreakpoint.js +75 -0
- package/dist/lib/useBreakpoint.js.map +1 -0
- package/dist/lib/validation.js +78 -0
- package/dist/lib/validation.js.map +1 -0
- package/dist/portal/ModalPortal.js +44 -0
- package/dist/portal/ModalPortal.js.map +1 -0
- package/dist/styles.css +132 -0
- package/dist/utils/localeFormat.js +82 -0
- package/dist/utils/localeFormat.js.map +1 -0
- package/docs/DESIGN-SYSTEM.md +668 -0
- package/package.json +87 -0
- package/tailwind/preset.js +157 -0
- package/types/index.d.ts +645 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `omniconn-design-system`.
|
|
4
|
+
|
|
5
|
+
This version line is continuous with the design system's history from before it
|
|
6
|
+
was extracted into its own package — `1.10.0` is the same `1.10.0` that shipped
|
|
7
|
+
inside the TAQA dashboard, so no call site had to change.
|
|
8
|
+
|
|
9
|
+
## 1.11.0 — 2026-08-19
|
|
10
|
+
|
|
11
|
+
Extracted from the TAQA dashboard into this standalone, publishable package. The
|
|
12
|
+
component APIs are unchanged — every existing call site keeps working — but the
|
|
13
|
+
library no longer reaches into an application for its dependencies.
|
|
14
|
+
|
|
15
|
+
### New
|
|
16
|
+
|
|
17
|
+
- **Packaging** — Published as `omniconn-design-system`. ESM, module graph preserved, so both `import { Button } from "omniconn-design-system"` and `import Button from "omniconn-design-system/components/Button"` work and tree-shake. `react`, `react-dom` and `@phosphor-icons/react` are peers.
|
|
18
|
+
- **Tailwind preset** — `omniconn-design-system/tailwind-preset` ships the colors, type scale, radii, shadows, gradients, screens, keyframes and the RTL / scrollbar-hide plugins. It reads `PALETTE` and `SCREENS` from the same modules the components import, so the config and the runtime resolvers still cannot drift.
|
|
19
|
+
- **Token stylesheet** — `omniconn-design-system/styles.css` carries the `:root` semantic role variables and the modal/drawer transition classes that `ModalPortal` needs. Previously these lived in the app's global CSS.
|
|
20
|
+
- **i18n seam** — `DesignSystemProvider`, `setTranslator`, `setLanguage` and `useI18n` let a host app supply its own translations. Components ship with English copy built in, so they render correctly with no setup at all.
|
|
21
|
+
- **Types** — Hand-written `types/index.d.ts` covering the full public API.
|
|
22
|
+
- **ModalPortal** — Now exported. It was previously an app-level component that `Modal` and `Drawer` reached sideways into.
|
|
23
|
+
- **Locale helpers** — `formatLocalizedNumber`, `getNumberLocale`, `isRtl` and the rest of the locale-format utilities are now part of the public API; `Table`, `Select`, `Pagination`, `DatePicker` and the validators all depend on them.
|
|
24
|
+
|
|
25
|
+
### Breaking
|
|
26
|
+
|
|
27
|
+
- **i18n** — The components no longer call `react-i18next` directly. An app that wants its own copy must register a translator (`setTranslator(t)` at startup, or wrap in `<DesignSystemProvider t language>`) instead of relying on an ambient i18next instance. Keys are unchanged (`common.*`), so a registered translator picks up existing bundles as-is. Apps that do nothing get English.
|
|
28
|
+
|
|
29
|
+
## 1.10.0 — 2026-08-12
|
|
30
|
+
|
|
31
|
+
Range mode's Start/End date and time, and the presets dropdown, were all-or-nothing — a caller could disable the whole picker but not lock just one endpoint. Five new props grey out and lock any one of them individually while the rest stays interactive.
|
|
32
|
+
|
|
33
|
+
### New
|
|
34
|
+
|
|
35
|
+
- **Date picker** — `disableStartDate` / `disableEndDate` grey out and lock that endpoint's date — the calendar then only lets the other one be picked (clicks that would move the locked side are ignored instead of redirected). `disableStartTime` / `disableEndTime` do the same for that endpoint's hour/minute fields, and `disablePresets` greys out the Last 24h/7d/30d/Custom dropdown. Applying a preset respects all of these — a locked endpoint's date/time no longer gets silently overwritten. A locked date needs an initial `value` for that side, or Apply has nothing to submit.
|
|
36
|
+
|
|
37
|
+
## 1.9.0 — 2026-08-12
|
|
38
|
+
|
|
39
|
+
Where an action appears was decided entirely by hardcoded Tailwind class pairs inside PageHeader and Card — a caller could not say "this one is for phones only" or "this one must never collapse". Actions now take two per-action props, `on` and `inline`, resolved against three shared breakpoint tiers. Every existing call site keeps its current layout: the defaults reproduce the old classes exactly.
|
|
40
|
+
|
|
41
|
+
### New
|
|
42
|
+
|
|
43
|
+
- **Page header** — Actions take `on` (where the action exists at all) and `inline` (where it's a button rather than a ⋮ row), over the tiers mobile <640 / tablet 640–1023 / desktop ≥1024. Grammar: 'all' | 'none' | a tier | a tier with + or - | an array of those. `primary` is now documented as sugar for inline: 'tablet+' plus a labelled button.
|
|
44
|
+
- **Card** — Card.Header actions take the same `on` / `inline` props as Page header, with the same grammar and defaults.
|
|
45
|
+
- **Layout** — useBreakpointTier() is the app's shared breakpoint hook — one matchMedia store for every consumer, correct on first render, and it only re-renders when a tier boundary is actually crossed. useMediaQuery / useIsBelow cover thresholds that aren't tier boundaries. Screen widths now live in ui/breakpoints.js, which tailwind.config.js imports, so the config and the runtime can't drift.
|
|
46
|
+
- **Select** — `inverse` styles the trigger, chips, placeholder and caret for a dark surface — same prop name and class values as SearchBar's inverse, and the same white/10 fill Button's inverse variant uses. Select was the only control with no dark-surface support, so putting one on a gradient header gave you a white box against the brand gradient. The dropdown panel stays light; `error` still wins over inverse.
|
|
47
|
+
|
|
48
|
+
### Improved
|
|
49
|
+
|
|
50
|
+
- **Page header** — A header now renders one actions row and one ⋮ menu instead of rendering several and hiding the wrong ones with CSS — tiered mode's two overlapping ActionMenus are gone, and actions that don't apply at the current width are absent from the DOM rather than hidden.
|
|
51
|
+
- **Page header** — A compact custom control can now sit on the title row on mobile: pass it as a one-item array with `render` + `inline: "all"`. A raw `actions` node still keeps its own row below 768px, since its width is unknown.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **Page header** — DEV now warns when an action uses `render` without a `label` and can still fold into the ⋮ at some width, where it would have rendered as a silently empty menu row.
|
|
56
|
+
- **Search bar** — Removed two dead `inverseClasses` variables that were computed and never applied — the inline ternaries below them were always doing the real work.
|
|
57
|
+
|
|
58
|
+
## 1.8.1 — 2026-08-06
|
|
59
|
+
|
|
60
|
+
Pagination, Table, Select, Search bar, Date picker, Modal, Drawer, Menu and Page header were entirely hardcoded English with no Arabic-Indic digit support — dropping any of them into a page and switching the app to Arabic changed nothing. They now translate their own text and localize every number they render.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- **Pagination** — "Showing X to Y of Z", rows-per-page, go-to and the nav button labels now translate, and every number (page counts, page buttons, rows-per-page options) renders in Arabic-Indic digits when the app is in Arabic.
|
|
65
|
+
- **Table** — Toolbar search, filter placeholders, the empty-state message, the "Actions" header and the selection banner now translate; selection and row counts localize.
|
|
66
|
+
- **Select** — Placeholder, in-panel search, "No results", and the clear/remove-chip labels now translate; the multi-select "N selected" summary localizes its count.
|
|
67
|
+
- **Search bar** — Placeholder and the clear/search aria-labels now translate.
|
|
68
|
+
- **Date picker** — Every label (Start/End, Custom, Apply, Time, nav) now translates, including the default preset names. Calendar day numbers and the year-grid header localize, and month/year names now follow the app's language instead of the browser's own locale.
|
|
69
|
+
- **Modal** — The footer's default Cancel label and the header's Close button now translate, reusing the shared `common.cancel` / `common.close` keys.
|
|
70
|
+
- **Drawer** — Same fix as Modal — the default Cancel label and Close button now translate.
|
|
71
|
+
- **Menu** — Menu.Profile's Close button and Avatar's edit-badge fallback label now translate. The shared ⋮ overflow menu (used here, in Card, and in Page header) now translates its "More actions" label too.
|
|
72
|
+
- **Page header** — The back button and the mobile Download action label now translate.
|
|
73
|
+
- **Form validation** — `validators.required/email/minLength/maxLength/min/max`'s default messages now translate and re-resolve on every validation run instead of freezing whatever language was active when the rules array was built; numeric thresholds in those messages localize too.
|
|
74
|
+
|
|
75
|
+
## 1.8.0 — 2026-08-06
|
|
76
|
+
|
|
77
|
+
Any color in the palette can now be used as a component tone, resolved through one shared module with automatic text contrast. Plus a Typography rebuild and rem-based sizing.
|
|
78
|
+
|
|
79
|
+
### Breaking
|
|
80
|
+
|
|
81
|
+
- **Tone system** — Auto-contrast applies to the existing named tones too, so `warning` headers now use dark text instead of white. White on `#F59E0B` was 2.15:1 and failed WCAG AA. The other eight tones render byte-identically to before.
|
|
82
|
+
|
|
83
|
+
### New
|
|
84
|
+
|
|
85
|
+
- **Tone system** — `tone.js` is the single source of truth for color: it resolves a family (`success`), an explicit shade (`primary-300`), or a status gradient to a background, and picks readable text automatically from the background's WCAG relative luminance.
|
|
86
|
+
- **Button** — New `color` prop takes any design-system token and overrides `variant`'s background, with the hover state resolving to the next step down its own ramp. `variant` is untouched, so existing call sites are unaffected.
|
|
87
|
+
- **Card** — `Card.Header`'s `tone` accepts any token, not just the nine named ones — the sub-header row and action buttons follow it automatically.
|
|
88
|
+
- **Modal** — `Modal.Header`'s `tone` accepts any token, and the footer's action button matches it — including explicit shades, which the old six-value lookup couldn't express.
|
|
89
|
+
- **Drawer** — `Drawer.Header`'s `tone` accepts any token, matching Modal exactly.
|
|
90
|
+
- **Table** — `headerTone` accepts any token. `"primary"` still means the brand gradient, so existing tables are unchanged.
|
|
91
|
+
- **Tabs** — New `tone` prop. A bare family keeps the tinted `-50` / `-700` look; an explicit shade fills solid and auto-contrasts.
|
|
92
|
+
- **Page header** — `tone` accepts any token, and `actions` now takes an array of `{ key, label, icon, onClick }` that renders as icon buttons on desktop and collapses into a ⋮ menu on mobile. Flag an action `primary: true` for tiered layout when a page has more actions than fit on one line. Passing `actions` as a node still works exactly as before.
|
|
93
|
+
- **Modal** — New `size="auto"` sizes the panel to its content, capped at 56rem. Best for content with intrinsic width (tables, images, short confirms) — a long paragraph won't wrap, it just widens the modal to the cap.
|
|
94
|
+
|
|
95
|
+
### Improved
|
|
96
|
+
|
|
97
|
+
- **Modal** — Below `sm` the modal docks to the bottom edge as a full-width sheet with square bottom corners, and footer buttons stack full-width with the primary action on top. `Drawer.Footer` got the same treatment.
|
|
98
|
+
- **Modal** — Sizes moved from px to rem (sm 33 · md 43 · lg 53 · xl 67 · 2xl 80). The root font on this app runs 12px at 1080p up to 20px at 4K, so px panels stayed physically small while their text grew. Drawer and Select's dropdown moved too. At 1080p every size lands within 4px of where it was.
|
|
99
|
+
- **Typography** — All 17 type tokens are documented, grouped into the brand `display`/`headline` scale and the app UI scale, with real line-height, letter-spacing and weight per row. Sizes are shown in rem — the previous px labels were only correct at a 16px root.
|
|
100
|
+
|
|
101
|
+
### Fixed
|
|
102
|
+
|
|
103
|
+
- **Page header** — The `icon` prop now actually renders. It was destructured and dropped, despite seven production pages passing one.
|
|
104
|
+
- **Page header** — The back button was hardcoded white-on-translucent-white, making it invisible on `tone="plain"`. It now follows the resolved tone. `onDownloadClick` no longer disappears below `sm` — it joins the ⋮ menu.
|
|
105
|
+
- **Menu** — A `render` action passed to `Card.Header` used to vanish entirely on mobile or past three actions, because the overflow menu filtered it out. It now falls back to the action's label and icon.
|
|
106
|
+
|
|
107
|
+
## 1.7.0 — 2026-08-06
|
|
108
|
+
|
|
109
|
+
A responsive pass over the data-display components — the table and pagination now hold up on a phone.
|
|
110
|
+
|
|
111
|
+
### Improved
|
|
112
|
+
|
|
113
|
+
- **Table** — The table gets a minimum width of 140px per column, so on narrow screens it scrolls horizontally instead of squishing columns to unreadable widths. Scroll shadows appear at whichever edge still has content.
|
|
114
|
+
- **Table** — The toolbar stacks vertically below `sm` and its trailing buttons wrap, instead of overflowing.
|
|
115
|
+
- **Table** — The stats row fits up to 8 cards on one line, stepping down through three density tiers as the count grows. Four or fewer cards render exactly as before.
|
|
116
|
+
- **Pagination** — Below `sm` the First/Last buttons and the "Go to page" field are hidden, the bar centres, and the button row wraps rather than clipping. Rows-per-page stays visible at every size.
|
|
117
|
+
|
|
118
|
+
### Fixed
|
|
119
|
+
|
|
120
|
+
- **Table** — Row hover and row dividers no longer break across the first column, and the stray vertical line on desktop is gone. The frozen first column was ultimately removed — the whole table slides instead, which is what caused each of these.
|
|
121
|
+
|
|
122
|
+
## 1.6.0 — 2026-08-06
|
|
123
|
+
|
|
124
|
+
The color system filled out: full ramps for every status color, four new gradients, and two generator tools.
|
|
125
|
+
|
|
126
|
+
### New
|
|
127
|
+
|
|
128
|
+
- **Colors** — Four status gradients — `bg-gradient-success`, `-warning`, `-error`, `-info` — alongside the existing header and primary gradients.
|
|
129
|
+
- **Colors** — A palette scheme generator: pick a base color and get a complementary, analogous, triadic, split-complementary, tetradic or monochromatic set, restricted to colors already in the system.
|
|
130
|
+
|
|
131
|
+
### Fixed
|
|
132
|
+
|
|
133
|
+
- **Colors** — `success`, `warning`, `error` and `info` now have full 50–950 ramps instead of only `{ DEFAULT, 50, 700 }`. Several files were already using shades like `bg-success-500` and `border-warning-200`, which produced no styling at all because Tailwind only emits keys that exist in config — those now render.
|
|
134
|
+
|
|
135
|
+
## 1.5.0 — 2026-08-06
|
|
136
|
+
|
|
137
|
+
Four new components, and two long-standing gaps in Select and Table closed.
|
|
138
|
+
|
|
139
|
+
### New
|
|
140
|
+
|
|
141
|
+
- **Drawer** — Edge-anchored slide-in panel for filters, detail views and forms that shouldn't fully interrupt the page. Same compound API as Modal — Header/Body/Footer, tones, footer action matching. `side` takes `start`/`end` (logical, mirrors in RTL), `top` or `bottom`.
|
|
142
|
+
- **Tabs** — Pill-track tab bar with optional icons, driven by `value`/`onChange`. Equal-width segments by default; `fullWidth={false}` sizes each tab to its content. Scrolls horizontally on narrow screens.
|
|
143
|
+
- **Menu** — Profile and action menus — usable as a static panel or anchored to a trigger. Portaled to `<body>` so it escapes any `overflow-hidden` ancestor, and positioned logically so it mirrors in RTL.
|
|
144
|
+
- **Segmented toggle** — Pick one of a small fixed set — a pill track, not a boolean. This is what the dashboard's Power/Energy switch actually is; it used to be two Buttons in a div with `!important` overrides.
|
|
145
|
+
- **Select** — Options support grouping via an optional `group` key, the equivalent of a native `<optgroup>`. Options without one render exactly as before.
|
|
146
|
+
- **Table** — A column with `filter: "select"` can set `multiple: false` for a plain single-choice dropdown. Multi-select stays the default, so existing filter columns are unchanged.
|
|
147
|
+
|
|
148
|
+
## 1.4.0 — 2026-08-06
|
|
149
|
+
|
|
150
|
+
Colored headers across Card, Modal and Table, and a real API reference on every component page.
|
|
151
|
+
|
|
152
|
+
### New
|
|
153
|
+
|
|
154
|
+
- **Card** — `Card.Header`'s `tone` grew from `gradient`/`plain` to nine values, adding the seven semantic colors. The sub-header row's text color follows the header automatically, so it no longer has to be hand-picked.
|
|
155
|
+
- **Modal** — `Modal.Header` takes the same nine tones as Card, and the footer's action button color-matches the header.
|
|
156
|
+
- **Table** — `headerTone` grew from `primary`/`plain` to seven values. `"primary"` keeps meaning the gradient, so nothing existing changed.
|
|
157
|
+
- **Design system site** — Every component page now ends with a Props table — name, type, default, description — and every code snippet has a copy button.
|
|
158
|
+
|
|
159
|
+
### Fixed
|
|
160
|
+
|
|
161
|
+
- **Card** — A `plain` header's sub-header row was showing the brand gradient, because its background was hardcoded rather than following the tone.
|
|
162
|
+
|
|
163
|
+
## 1.3.0 — 2026-08-06
|
|
164
|
+
|
|
165
|
+
The page-title banner became a component, and the design system got a getting-started guide.
|
|
166
|
+
|
|
167
|
+
### New
|
|
168
|
+
|
|
169
|
+
- **Page header** — One page-title banner replacing the gradient header block copy-pasted across Users, Alarms, Feedback, Settings and Event Scheduling. Title, optional subtitle, optional icon, actions, three sizes, gradient or plain.
|
|
170
|
+
- **Guidelines** — Getting started, the conventions every component shares, and the rules that keep the system consistent.
|
|
171
|
+
- **Tooling** — The `@/` import alias is real now — `import { Button } from "@/components/ui"` resolves. Every snippet on this site had been showing it before it worked.
|
|
172
|
+
|
|
173
|
+
### Improved
|
|
174
|
+
|
|
175
|
+
- **Table** — The existing `paginated={false}` prop is documented and demoed. It renders every row and omits the pagination bar — it worked all along, but wasn't discoverable.
|
|
176
|
+
|
|
177
|
+
## 1.2.0 — 2026-08-06
|
|
178
|
+
|
|
179
|
+
A custom date picker — single date by default, with range, presets and time-of-day.
|
|
180
|
+
|
|
181
|
+
### New
|
|
182
|
+
|
|
183
|
+
- **Date picker** — Custom calendar popover, built from scratch to match the tokens rather than wrapping react-datepicker. Single date by default; `range` adds a presets dropdown, Start/End fields and Apply.
|
|
184
|
+
- **Date picker** — `showTime` adds 24-hour HH:MM fields — one pair in single mode behind an OK button, one per endpoint in range mode.
|
|
185
|
+
- **Date picker** — `disableFuture` caps the picker at the current moment: future days are disabled, and with `showTime` the hour/minute fields clamp once today is selected.
|
|
186
|
+
|
|
187
|
+
### Fixed
|
|
188
|
+
|
|
189
|
+
- **Date picker** — "Last 24 hours" resolved to midnight-to-midnight of the same day — a zero-length range that never reached back a day. All three presets are now precise rolling windows anchored to the current moment.
|
|
190
|
+
|
|
191
|
+
## 1.1.0 — 2026-08-06
|
|
192
|
+
|
|
193
|
+
Data display: one config-driven table and one pagination bar, replacing nine and six variants respectively.
|
|
194
|
+
|
|
195
|
+
### New
|
|
196
|
+
|
|
197
|
+
- **Table** — Config-driven and client-side — declare columns, hand it an array, and get sorting, global search, per-column inline search, column-derived filters, a stats row, row actions and pagination.
|
|
198
|
+
- **Table** — `selectable` adds checkbox selection with a bulk-action bar and Gmail-style "select all N matching" across pages. Selection survives live data updates instead of being wiped by them.
|
|
199
|
+
- **Pagination** — "Showing X to Y of Z" plus first/prev/numbered-window/next/last, with arrows mirrored in RTL. Renders nothing when there's no data.
|
|
200
|
+
|
|
201
|
+
## 1.0.0 — 2026-08-06
|
|
202
|
+
|
|
203
|
+
First release: the form controls, built custom against the tokens rather than pulled from a component library.
|
|
204
|
+
|
|
205
|
+
### New
|
|
206
|
+
|
|
207
|
+
- **Select** — Searchable, optionally multi-select, with icons or images in options. Portaled to `<body>`, so it isn't clipped inside a modal or any `overflow-hidden` container.
|
|
208
|
+
- **Checkbox** — Custom box with an indeterminate state, backed by a hidden native input for form and accessibility behaviour.
|
|
209
|
+
- **Radio** — `RadioGroup` driven by an options array, with optional per-option description and icon.
|
|
210
|
+
- **Switch** — On/off toggle with a knob that translates logically, so it mirrors in RTL.
|
|
211
|
+
- **Search bar** — Leading icon, clear button, Enter-to-commit and optional debounce.
|
|
212
|
+
- **Text inputs** — `Textarea` joins `Field` and `Input`, with `resize` and `autoGrow`. Every control in this release is Field-compatible — it accepts the `id`, `error`, `required` and `aria-*` props that `Field` injects.
|
|
213
|
+
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Omniconn
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# omniconn-design-system
|
|
2
|
+
|
|
3
|
+
A React + Tailwind design system: 22 components, a token palette, and a Tailwind
|
|
4
|
+
preset. Everything is built from scratch against the tokens — no Radix, no
|
|
5
|
+
headless-UI wrapper, no `class-variance-authority`. The only runtime peers are
|
|
6
|
+
React and Phosphor Icons.
|
|
7
|
+
|
|
8
|
+
Every component is RTL-aware (logical properties throughout, not `left`/`right`),
|
|
9
|
+
and every string it renders can be translated.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install omniconn-design-system
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Peer dependencies you also need:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install react react-dom @phosphor-icons/react tailwindcss
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
Three steps: the preset, the stylesheet, and the content glob.
|
|
26
|
+
|
|
27
|
+
**1. Extend your Tailwind config with the preset.**
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
// tailwind.config.js
|
|
31
|
+
import preset from "omniconn-design-system/tailwind-preset";
|
|
32
|
+
|
|
33
|
+
export default {
|
|
34
|
+
presets: [preset],
|
|
35
|
+
content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"],
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The preset brings the color ramps, the type scale, the radius scale, focus-ring
|
|
40
|
+
shadows, brand gradients, screen widths, and the `tailwindcss-rtl` and
|
|
41
|
+
`tailwind-scrollbar-hide` plugins.
|
|
42
|
+
|
|
43
|
+
It also appends the package's own `dist` to `content`, so the classes the
|
|
44
|
+
components emit survive purging. If your install layout is unusual (pnpm without
|
|
45
|
+
hoisting, a monorepo with a shared root `node_modules`), add the real path
|
|
46
|
+
yourself:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
content: [
|
|
50
|
+
"./src/**/*.{js,jsx,ts,tsx}",
|
|
51
|
+
"./node_modules/omniconn-design-system/dist/**/*.js",
|
|
52
|
+
],
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**2. Import the stylesheet**, above your Tailwind directives:
|
|
56
|
+
|
|
57
|
+
```css
|
|
58
|
+
/* src/index.css */
|
|
59
|
+
@import "omniconn-design-system/styles.css";
|
|
60
|
+
|
|
61
|
+
@tailwind base;
|
|
62
|
+
@tailwind components;
|
|
63
|
+
@tailwind utilities;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This carries the `:root` semantic role variables (`--color-primary`,
|
|
67
|
+
`--color-surface`, `--color-content`, …) that the preset's color roles point at,
|
|
68
|
+
plus the transition classes `Modal` and `Drawer` animate with. Without it,
|
|
69
|
+
overlays still work but pop in with no animation.
|
|
70
|
+
|
|
71
|
+
**3. Use the components.**
|
|
72
|
+
|
|
73
|
+
```jsx
|
|
74
|
+
import { Button, Card, Table } from "omniconn-design-system";
|
|
75
|
+
import { Gauge } from "@phosphor-icons/react";
|
|
76
|
+
|
|
77
|
+
export default function Devices({ rows }) {
|
|
78
|
+
return (
|
|
79
|
+
<Card>
|
|
80
|
+
<Card.Header icon={Gauge} title="Devices" />
|
|
81
|
+
<Card.Body>
|
|
82
|
+
<Table
|
|
83
|
+
columns={[
|
|
84
|
+
{ key: "name", header: "Name", sortable: true },
|
|
85
|
+
{ key: "status", header: "Status", filter: "select" },
|
|
86
|
+
]}
|
|
87
|
+
data={rows}
|
|
88
|
+
rowKey="id"
|
|
89
|
+
searchable
|
|
90
|
+
/>
|
|
91
|
+
</Card.Body>
|
|
92
|
+
</Card>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## What's in it
|
|
98
|
+
|
|
99
|
+
| Group | Components |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| Actions | `Button`, `Menu`, `ActionMenu` |
|
|
102
|
+
| Layout / chrome | `Card`, `PageHeader`, `Modal`, `Drawer`, `Tabs` |
|
|
103
|
+
| Form controls | `Field`, `Input`, `Textarea`, `Select`, `Checkbox`, `RadioGroup`, `Radio`, `Switch`, `SegmentedToggle`, `DatePicker`, `SearchBar` |
|
|
104
|
+
| Data display | `Table`, `Pagination`, `Badge`, `Avatar` |
|
|
105
|
+
|
|
106
|
+
Plus `cn`, the `validators` / `useField` form-validation kit, the `PALETTE` and
|
|
107
|
+
tone resolvers, the `useBreakpointTier` responsive primitives, and the
|
|
108
|
+
locale-formatting helpers.
|
|
109
|
+
|
|
110
|
+
Full prop reference: [`docs/DESIGN-SYSTEM.md`](./docs/DESIGN-SYSTEM.md).
|
|
111
|
+
|
|
112
|
+
## Theming
|
|
113
|
+
|
|
114
|
+
The semantic roles are CSS variables, so rebranding is a stylesheet override —
|
|
115
|
+
no Tailwind rebuild, no component changes:
|
|
116
|
+
|
|
117
|
+
```css
|
|
118
|
+
:root {
|
|
119
|
+
--color-primary: #0f766e;
|
|
120
|
+
--color-primary-hover: #0d5f58;
|
|
121
|
+
--color-surface: #ffffff;
|
|
122
|
+
--color-content: #0d1013;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The raw ramps (`primary-50` … `primary-950`) live in JS, in `src/lib/tone.js`,
|
|
127
|
+
because some components resolve a color at runtime and set it as an inline
|
|
128
|
+
style — Tailwind can't generate a class from a runtime string. Both the preset
|
|
129
|
+
and the components read that same module, so they can't drift.
|
|
130
|
+
|
|
131
|
+
## Translations
|
|
132
|
+
|
|
133
|
+
Components ship with English copy built in and work with no configuration. To
|
|
134
|
+
supply your own, register a translator once at startup:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
import { setTranslator, setLanguage } from "omniconn-design-system";
|
|
138
|
+
import i18n from "./i18n";
|
|
139
|
+
|
|
140
|
+
setTranslator((key, vars) => i18n.t(key, vars), { language: i18n.language });
|
|
141
|
+
i18n.on("languageChanged", setLanguage);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Or wrap your tree, if you'd rather keep it declarative:
|
|
145
|
+
|
|
146
|
+
```jsx
|
|
147
|
+
import { useTranslation } from "react-i18next";
|
|
148
|
+
import { DesignSystemProvider } from "omniconn-design-system";
|
|
149
|
+
|
|
150
|
+
const { t, i18n } = useTranslation();
|
|
151
|
+
<DesignSystemProvider t={t} language={i18n.language}>
|
|
152
|
+
<App />
|
|
153
|
+
</DesignSystemProvider>;
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Keys are namespaced under `common.*` (`common.table.noData`,
|
|
157
|
+
`common.pagination.showing`, …) and use i18next-style `{{name}}` placeholders,
|
|
158
|
+
so an existing i18next bundle works as-is. The full key list is in
|
|
159
|
+
[`src/i18n/strings.en.js`](./src/i18n/strings.en.js); an Arabic bundle ships
|
|
160
|
+
alongside it and can be passed as `resources` or merged into your own.
|
|
161
|
+
|
|
162
|
+
`language` drives more than copy — it selects the numeral system, so Arabic
|
|
163
|
+
renders Arabic-Indic digits in tables, pagination and the date picker.
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
npm install
|
|
169
|
+
npm run build # dist/ (JS + sourcemaps) and dist/styles.css
|
|
170
|
+
npm run lint
|
|
171
|
+
npm run typecheck # validates types/index.d.ts
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The build preserves the module graph rather than producing one bundle, so deep
|
|
175
|
+
imports resolve and consumers tree-shake down to what they actually use. The
|
|
176
|
+
type definitions are hand-maintained — update `types/index.d.ts` in the same
|
|
177
|
+
change as any prop addition.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useCallback, useLayoutEffect, useEffect } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { useI18n } from "../i18n/translator.js";
|
|
5
|
+
import { DotsThreeVertical } from "@phosphor-icons/react";
|
|
6
|
+
import Button from "./Button.js";
|
|
7
|
+
import { cn } from "../lib/cn.js";
|
|
8
|
+
function ActionMenu({ actions = [], isDark, className }) {
|
|
9
|
+
const { t } = useI18n();
|
|
10
|
+
const [open, setOpen] = useState(false);
|
|
11
|
+
const [pos, setPos] = useState(null);
|
|
12
|
+
const triggerRef = useRef(null);
|
|
13
|
+
const panelRef = useRef(null);
|
|
14
|
+
const updatePosition = useCallback(() => {
|
|
15
|
+
const el = triggerRef.current;
|
|
16
|
+
if (!el) return;
|
|
17
|
+
const r = el.getBoundingClientRect();
|
|
18
|
+
const isRtl = getComputedStyle(el).direction === "rtl";
|
|
19
|
+
setPos(
|
|
20
|
+
isRtl ? { top: r.bottom + 4, left: r.left } : { top: r.bottom + 4, right: window.innerWidth - r.right }
|
|
21
|
+
);
|
|
22
|
+
}, []);
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
if (!open) return;
|
|
25
|
+
updatePosition();
|
|
26
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
27
|
+
window.addEventListener("resize", updatePosition);
|
|
28
|
+
return () => {
|
|
29
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
30
|
+
window.removeEventListener("resize", updatePosition);
|
|
31
|
+
};
|
|
32
|
+
}, [open, updatePosition]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!open) return;
|
|
35
|
+
const onDown = (e) => {
|
|
36
|
+
if (triggerRef.current?.contains(e.target) || panelRef.current?.contains(e.target)) return;
|
|
37
|
+
if (e.target.closest?.("[data-ds-popover]")) return;
|
|
38
|
+
setOpen(false);
|
|
39
|
+
};
|
|
40
|
+
document.addEventListener("mousedown", onDown);
|
|
41
|
+
return () => document.removeEventListener("mousedown", onDown);
|
|
42
|
+
}, [open]);
|
|
43
|
+
if (actions.length === 0) return null;
|
|
44
|
+
return /* @__PURE__ */ jsxs("div", { className, children: [
|
|
45
|
+
/* @__PURE__ */ jsx("span", { ref: triggerRef, className: "inline-flex", children: /* @__PURE__ */ jsx(
|
|
46
|
+
Button,
|
|
47
|
+
{
|
|
48
|
+
variant: isDark ? "inverse" : "ghost",
|
|
49
|
+
size: "sm",
|
|
50
|
+
iconOnly: true,
|
|
51
|
+
icon: DotsThreeVertical,
|
|
52
|
+
"aria-label": t("common.actionMenu.moreActions"),
|
|
53
|
+
"aria-haspopup": "menu",
|
|
54
|
+
"aria-expanded": open,
|
|
55
|
+
onClick: () => setOpen((o) => !o)
|
|
56
|
+
}
|
|
57
|
+
) }),
|
|
58
|
+
open && pos && createPortal(
|
|
59
|
+
/* @__PURE__ */ jsx(
|
|
60
|
+
"div",
|
|
61
|
+
{
|
|
62
|
+
ref: panelRef,
|
|
63
|
+
role: "menu",
|
|
64
|
+
"data-ds-popover": true,
|
|
65
|
+
style: {
|
|
66
|
+
position: "fixed",
|
|
67
|
+
top: pos.top,
|
|
68
|
+
left: pos.left,
|
|
69
|
+
right: pos.right,
|
|
70
|
+
zIndex: 10050
|
|
71
|
+
},
|
|
72
|
+
className: "min-w-[10rem] bg-surface border border-border rounded-md shadow-lg overflow-hidden py-1",
|
|
73
|
+
children: actions.map((a) => /* @__PURE__ */ jsxs(
|
|
74
|
+
"button",
|
|
75
|
+
{
|
|
76
|
+
type: "button",
|
|
77
|
+
role: "menuitem",
|
|
78
|
+
disabled: a.disabled,
|
|
79
|
+
onClick: () => {
|
|
80
|
+
a.onClick?.();
|
|
81
|
+
setOpen(false);
|
|
82
|
+
},
|
|
83
|
+
className: cn(
|
|
84
|
+
"w-full flex items-center gap-2.5 px-3 py-2 text-start text-body text-content transition",
|
|
85
|
+
"hover:bg-neutral-100",
|
|
86
|
+
a.disabled && "opacity-50 cursor-not-allowed"
|
|
87
|
+
),
|
|
88
|
+
children: [
|
|
89
|
+
a.icon && /* @__PURE__ */ jsx(a.icon, { size: 16, weight: "regular", className: "text-content-muted shrink-0" }),
|
|
90
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: a.label })
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
a.key
|
|
94
|
+
))
|
|
95
|
+
}
|
|
96
|
+
),
|
|
97
|
+
document.body
|
|
98
|
+
)
|
|
99
|
+
] });
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
ActionMenu as default
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=ActionMenu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActionMenu.js","sources":["../../src/components/ActionMenu.jsx"],"sourcesContent":["import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useI18n } from \"../i18n/translator\";\nimport { DotsThreeVertical } from \"@phosphor-icons/react\";\nimport Button from \"./Button\";\nimport { cn } from \"../lib/cn\";\n\n/**\n * ActionMenu — the shared \"⋮\" overflow menu used by Card.Header and PageHeader.\n *\n * Takes the same action shape those components use:\n * [{ key, label, icon, onClick, disabled? }]\n *\n * It is deliberately not breakpoint-aware. Callers resolve which actions belong\n * in the menu at the current tier (see ./actionVisibility.js) and pass only\n * those; an empty list renders nothing, so there is exactly one menu per header\n * at any width. `className` is still yours for spacing or ordering — just don't\n * use it to hide the menu at a breakpoint, or the two systems will disagree.\n *\n * The panel is portaled to <body> so it isn't clipped by a parent's\n * overflow-hidden, and is marked `data-ds-popover` so other popovers'\n * outside-click handlers ignore clicks landing inside it.\n *\n * Positioning is logical: the panel pins to whichever edge the trigger's own\n * computed `direction` makes the \"end\" one, so it mirrors correctly in RTL.\n */\nexport default function ActionMenu({ actions = [], isDark, className }) {\n const { t } = useI18n();\n const [open, setOpen] = useState(false);\n const [pos, setPos] = useState(null);\n const triggerRef = useRef(null);\n const panelRef = useRef(null);\n\n const updatePosition = useCallback(() => {\n const el = triggerRef.current;\n if (!el) return;\n const r = el.getBoundingClientRect();\n // Pin to the trigger's end edge — start edge in RTL, end edge in LTR.\n const isRtl = getComputedStyle(el).direction === \"rtl\";\n setPos(\n isRtl\n ? { top: r.bottom + 4, left: r.left }\n : { top: r.bottom + 4, right: window.innerWidth - r.right }\n );\n }, []);\n\n useLayoutEffect(() => {\n if (!open) return;\n updatePosition();\n window.addEventListener(\"scroll\", updatePosition, true);\n window.addEventListener(\"resize\", updatePosition);\n return () => {\n window.removeEventListener(\"scroll\", updatePosition, true);\n window.removeEventListener(\"resize\", updatePosition);\n };\n }, [open, updatePosition]);\n\n useEffect(() => {\n if (!open) return;\n const onDown = (e) => {\n if (triggerRef.current?.contains(e.target) || panelRef.current?.contains(e.target)) return;\n if (e.target.closest?.(\"[data-ds-popover]\")) return;\n setOpen(false);\n };\n document.addEventListener(\"mousedown\", onDown);\n return () => document.removeEventListener(\"mousedown\", onDown);\n }, [open]);\n\n if (actions.length === 0) return null;\n\n return (\n <div className={className}>\n <span ref={triggerRef} className=\"inline-flex\">\n <Button\n variant={isDark ? \"inverse\" : \"ghost\"}\n size=\"sm\"\n iconOnly\n icon={DotsThreeVertical}\n aria-label={t(\"common.actionMenu.moreActions\")}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n onClick={() => setOpen((o) => !o)}\n />\n </span>\n\n {open &&\n pos &&\n createPortal(\n <div\n ref={panelRef}\n role=\"menu\"\n data-ds-popover\n style={{\n position: \"fixed\",\n top: pos.top,\n left: pos.left,\n right: pos.right,\n zIndex: 10050,\n }}\n className=\"min-w-[10rem] bg-surface border border-border rounded-md shadow-lg overflow-hidden py-1\"\n >\n {actions.map((a) => (\n <button\n key={a.key}\n type=\"button\"\n role=\"menuitem\"\n disabled={a.disabled}\n onClick={() => {\n a.onClick?.();\n setOpen(false);\n }}\n className={cn(\n \"w-full flex items-center gap-2.5 px-3 py-2 text-start text-body text-content transition\",\n \"hover:bg-neutral-100\",\n a.disabled && \"opacity-50 cursor-not-allowed\"\n )}\n >\n {a.icon && <a.icon size={16} weight=\"regular\" className=\"text-content-muted shrink-0\" />}\n <span className=\"truncate\">{a.label}</span>\n </button>\n ))}\n </div>,\n document.body\n )}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
import { useI18n } from "../i18n/translator.js";
|
|
4
|
+
import { User } from "@phosphor-icons/react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { resolveTone } from "../lib/tone.js";
|
|
7
|
+
const SIZES = { sm: 24, md: 32, lg: 40, xl: 70 };
|
|
8
|
+
const INITIALS_CLASS = {
|
|
9
|
+
sm: "text-caption",
|
|
10
|
+
md: "text-caption",
|
|
11
|
+
lg: "text-body-strong",
|
|
12
|
+
xl: "text-section font-display"
|
|
13
|
+
};
|
|
14
|
+
const BADGE_PX = { sm: 12, md: 16, lg: 20, xl: 25 };
|
|
15
|
+
const BADGE_ICON_PX = { sm: 8, md: 10, lg: 12, xl: 16 };
|
|
16
|
+
function initialsOf(name) {
|
|
17
|
+
if (typeof name !== "string") return null;
|
|
18
|
+
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
19
|
+
if (words.length === 0) return null;
|
|
20
|
+
const first = words[0][0];
|
|
21
|
+
const last = words.length > 1 ? words[words.length - 1][0] : "";
|
|
22
|
+
return (first + last).toUpperCase();
|
|
23
|
+
}
|
|
24
|
+
function Avatar({
|
|
25
|
+
src,
|
|
26
|
+
name,
|
|
27
|
+
size = "md",
|
|
28
|
+
color,
|
|
29
|
+
badge,
|
|
30
|
+
alt,
|
|
31
|
+
className = "",
|
|
32
|
+
...rest
|
|
33
|
+
}) {
|
|
34
|
+
const { t: tr } = useI18n();
|
|
35
|
+
const [failed, setFailed] = useState(false);
|
|
36
|
+
const px = SIZES[size] ?? SIZES.md;
|
|
37
|
+
const initials = initialsOf(name);
|
|
38
|
+
const showImage = Boolean(src) && !failed;
|
|
39
|
+
const t = color ? resolveTone(color, "neutral") : null;
|
|
40
|
+
const BadgeIcon = badge?.icon;
|
|
41
|
+
const badgePx = BADGE_PX[size] ?? BADGE_PX.md;
|
|
42
|
+
return /* @__PURE__ */ jsxs(
|
|
43
|
+
"span",
|
|
44
|
+
{
|
|
45
|
+
className: cn("relative inline-flex shrink-0", className),
|
|
46
|
+
style: { height: px, width: px },
|
|
47
|
+
...rest,
|
|
48
|
+
children: [
|
|
49
|
+
showImage ? /* @__PURE__ */ jsx(
|
|
50
|
+
"img",
|
|
51
|
+
{
|
|
52
|
+
src,
|
|
53
|
+
alt: alt ?? name ?? "",
|
|
54
|
+
onError: () => setFailed(true),
|
|
55
|
+
className: "rounded-full object-cover bg-neutral-100",
|
|
56
|
+
style: { height: px, width: px }
|
|
57
|
+
}
|
|
58
|
+
) : /* @__PURE__ */ jsx(
|
|
59
|
+
"span",
|
|
60
|
+
{
|
|
61
|
+
className: cn(
|
|
62
|
+
"rounded-full inline-flex items-center justify-center select-none font-medium",
|
|
63
|
+
!t && "bg-neutral-100 text-content-muted",
|
|
64
|
+
t?.className,
|
|
65
|
+
INITIALS_CLASS[size] ?? INITIALS_CLASS.md
|
|
66
|
+
),
|
|
67
|
+
style: { height: px, width: px, ...t?.style, ...t?.textStyle },
|
|
68
|
+
"aria-label": initials ? void 0 : alt ?? name ?? void 0,
|
|
69
|
+
role: initials ? void 0 : "img",
|
|
70
|
+
children: initials ?? /* @__PURE__ */ jsx(User, { size: Math.round(px * 0.55), weight: "regular" })
|
|
71
|
+
}
|
|
72
|
+
),
|
|
73
|
+
BadgeIcon && React.createElement(
|
|
74
|
+
badge.onClick ? "button" : "span",
|
|
75
|
+
{
|
|
76
|
+
type: badge.onClick ? "button" : void 0,
|
|
77
|
+
onClick: badge.onClick,
|
|
78
|
+
"aria-label": badge.onClick ? badge.label ?? tr("common.avatar.edit") : void 0,
|
|
79
|
+
title: badge.label,
|
|
80
|
+
className: cn(
|
|
81
|
+
"absolute bottom-0 end-0 rounded-full bg-primary text-white",
|
|
82
|
+
"inline-flex items-center justify-center border-2 border-surface",
|
|
83
|
+
"focus:outline-none focus-visible:shadow-focus",
|
|
84
|
+
badge.onClick && "hover:bg-primary-hover transition cursor-pointer"
|
|
85
|
+
),
|
|
86
|
+
style: { height: badgePx, width: badgePx }
|
|
87
|
+
},
|
|
88
|
+
/* @__PURE__ */ jsx(BadgeIcon, { size: BADGE_ICON_PX[size] ?? BADGE_ICON_PX.md, weight: "regular" })
|
|
89
|
+
)
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
Avatar as default
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=Avatar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Avatar.js","sources":["../../src/components/Avatar.jsx"],"sourcesContent":["import React, { useState } from \"react\";\nimport { useI18n } from \"../i18n/translator\";\nimport { User } from \"@phosphor-icons/react\";\nimport { cn } from \"../lib/cn\";\nimport { resolveTone } from \"../lib/tone\";\n\n/**\n * Avatar — circular user image with graceful fallbacks.\n *\n * Falls back in this order: `src` (image) → `name` (initials) → a User glyph.\n * A broken image URL falls through to the same chain at runtime, so a dead\n * profile picture never leaves an empty ring.\n *\n * Anatomy:\n * <Avatar\n * src=\"https://…/me.jpg\"\n * name=\"Your name\" // alt text + initials fallback\n * size=\"lg\" // 'sm' | 'md' | 'lg' | 'xl'\n * badge={{ icon: PencilSimple, onClick: () => {}, label: \"Change photo\" }}\n * />\n *\n * Props:\n * src image url; omit (or let it fail) to use the fallback chain\n * name used for `alt` and to derive initials — first letter of the first\n * and last word, uppercased (\"Your name\" → \"YN\")\n * size 'sm' 24px | 'md' 32px | 'lg' 40px | 'xl' 70px (default 'md')\n * color any design-system token for the initials/icon fallback background —\n * 'primary', 'primary-50', 'neutral-800'. The initials auto-contrast.\n * Ignored once an image loads. Default is the neutral-100 chip.\n * badge { icon, onClick?, label? } — small circular affordance pinned to the\n * bottom-end corner (the Figma \"edit photo\" pencil). Renders a <button>\n * when `onClick` is given, otherwise a decorative <span>.\n * alt overrides the derived alt text\n * className, ...rest applied to the root wrapper\n *\n * Sizes are inline styles rather than Tailwind classes: these are avatar\n * diameters, not the h-8/h-10/h-12 control heights the rest of the system uses,\n * and `cn()` is a plain joiner so a class would be fighting the caller's own.\n */\n\nconst SIZES = { sm: 24, md: 32, lg: 40, xl: 70 };\n\n// Initials get their own step so they stay legible at 24px and don't look\n// oversized at 70px.\nconst INITIALS_CLASS = {\n sm: \"text-caption\",\n md: \"text-caption\",\n lg: \"text-body-strong\",\n xl: \"text-section font-display\",\n};\n\nconst BADGE_PX = { sm: 12, md: 16, lg: 20, xl: 25 };\nconst BADGE_ICON_PX = { sm: 8, md: 10, lg: 12, xl: 16 };\n\n/** \"Your name\" → \"YN\"; \"Cher\" → \"C\"; \"\" → null. */\nfunction initialsOf(name) {\n if (typeof name !== \"string\") return null;\n const words = name.trim().split(/\\s+/).filter(Boolean);\n if (words.length === 0) return null;\n const first = words[0][0];\n const last = words.length > 1 ? words[words.length - 1][0] : \"\";\n return (first + last).toUpperCase();\n}\n\nexport default function Avatar({\n src,\n name,\n size = \"md\",\n color,\n badge,\n alt,\n className = \"\",\n ...rest\n}) {\n const { t: tr } = useI18n();\n const [failed, setFailed] = useState(false);\n const px = SIZES[size] ?? SIZES.md;\n const initials = initialsOf(name);\n const showImage = Boolean(src) && !failed;\n\n // Only the fallback is tintable — an image covers the whole circle anyway.\n const t = color ? resolveTone(color, \"neutral\") : null;\n\n const BadgeIcon = badge?.icon;\n const badgePx = BADGE_PX[size] ?? BADGE_PX.md;\n\n return (\n <span\n className={cn(\"relative inline-flex shrink-0\", className)}\n style={{ height: px, width: px }}\n {...rest}\n >\n {showImage ? (\n <img\n src={src}\n alt={alt ?? name ?? \"\"}\n onError={() => setFailed(true)}\n className=\"rounded-full object-cover bg-neutral-100\"\n style={{ height: px, width: px }}\n />\n ) : (\n <span\n className={cn(\n \"rounded-full inline-flex items-center justify-center select-none font-medium\",\n !t && \"bg-neutral-100 text-content-muted\",\n t?.className,\n INITIALS_CLASS[size] ?? INITIALS_CLASS.md\n )}\n style={{ height: px, width: px, ...t?.style, ...t?.textStyle }}\n aria-label={initials ? undefined : alt ?? name ?? undefined}\n role={initials ? undefined : \"img\"}\n >\n {initials ?? <User size={Math.round(px * 0.55)} weight=\"regular\" />}\n </span>\n )}\n\n {BadgeIcon &&\n React.createElement(\n badge.onClick ? \"button\" : \"span\",\n {\n type: badge.onClick ? \"button\" : undefined,\n onClick: badge.onClick,\n \"aria-label\": badge.onClick ? badge.label ?? tr(\"common.avatar.edit\") : undefined,\n title: badge.label,\n className: cn(\n \"absolute bottom-0 end-0 rounded-full bg-primary text-white\",\n \"inline-flex items-center justify-center border-2 border-surface\",\n \"focus:outline-none focus-visible:shadow-focus\",\n badge.onClick && \"hover:bg-primary-hover transition cursor-pointer\"\n ),\n style: { height: badgePx, width: badgePx },\n },\n <BadgeIcon size={BADGE_ICON_PX[size] ?? BADGE_ICON_PX.md} weight=\"regular\" />\n )}\n </span>\n );\n}\n"],"names":[],"mappings":"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"}
|