plass-ui 1.0.2 → 1.1.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 +169 -0
- package/README.md +14 -15
- package/dist/components/accordion/PlAccordion.d.ts +14 -0
- package/dist/components/accordion/PlAccordion.js +1 -1
- package/dist/components/avatar/PlAvatar.js +1 -1
- package/dist/components/avatar-group/PlAvatarGroup.d.ts +47 -0
- package/dist/components/avatar-group/PlAvatarGroup.js +1 -0
- package/dist/components/avatar-group/index.d.ts +2 -0
- package/dist/components/avatar-group/index.js +1 -0
- package/dist/components/checkbox/PlCheckbox.js +1 -1
- package/dist/components/color-picker/PlColorPicker.d.ts +88 -0
- package/dist/components/color-picker/PlColorPicker.js +1 -0
- package/dist/components/color-picker/index.d.ts +2 -0
- package/dist/components/color-picker/index.js +1 -0
- package/dist/components/combobox/PlCombobox.js +1 -1
- package/dist/components/command-palette/PlCommandPalette.d.ts +83 -0
- package/dist/components/command-palette/PlCommandPalette.js +1 -0
- package/dist/components/command-palette/index.d.ts +2 -0
- package/dist/components/command-palette/index.js +1 -0
- package/dist/components/divider/PlDivider.js +1 -1
- package/dist/components/drawer/PlDrawer.js +1 -1
- package/dist/components/fieldset/PlFieldset.d.ts +35 -0
- package/dist/components/fieldset/PlFieldset.js +1 -0
- package/dist/components/fieldset/index.d.ts +2 -0
- package/dist/components/fieldset/index.js +1 -0
- package/dist/components/footer/PlFooter.d.ts +90 -0
- package/dist/components/footer/PlFooter.js +1 -0
- package/dist/components/footer/index.d.ts +2 -0
- package/dist/components/footer/index.js +1 -0
- package/dist/components/form/PlForm.d.ts +61 -0
- package/dist/components/form/PlForm.js +1 -0
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.js +1 -0
- package/dist/components/header/PlHeader.d.ts +124 -0
- package/dist/components/header/PlHeader.js +1 -0
- package/dist/components/header/index.d.ts +2 -0
- package/dist/components/header/index.js +1 -0
- package/dist/components/menu/PlMenu.js +1 -1
- package/dist/components/menubar/PlMenubar.d.ts +56 -0
- package/dist/components/menubar/PlMenubar.js +1 -0
- package/dist/components/menubar/index.d.ts +2 -0
- package/dist/components/menubar/index.js +1 -0
- package/dist/components/modal/PlModal.js +1 -1
- package/dist/components/navigation-menu/PlNavigationMenu.d.ts +99 -0
- package/dist/components/navigation-menu/PlNavigationMenu.js +1 -0
- package/dist/components/navigation-menu/index.d.ts +2 -0
- package/dist/components/navigation-menu/index.js +1 -0
- package/dist/components/number-field/PlNumberField.js +1 -1
- package/dist/components/otp-field/PlOtpField.js +1 -1
- package/dist/components/overlay/PlOverlay.js +1 -1
- package/dist/components/page-layout/PlPageLayout.d.ts +139 -0
- package/dist/components/page-layout/PlPageLayout.js +1 -0
- package/dist/components/page-layout/index.d.ts +2 -0
- package/dist/components/page-layout/index.js +1 -0
- package/dist/components/pagination/PlPagination.d.ts +23 -0
- package/dist/components/pagination/PlPagination.js +1 -1
- package/dist/components/popover/PlPopover.js +1 -1
- package/dist/components/radio-group/PlRadioGroup.js +1 -1
- package/dist/components/scroll-zone/PlScrollZone.d.ts +35 -15
- package/dist/components/scroll-zone/PlScrollZone.js +1 -1
- package/dist/components/select/PlSelect.js +1 -1
- package/dist/components/sidebar/PlSidebar.d.ts +141 -0
- package/dist/components/sidebar/PlSidebar.js +1 -0
- package/dist/components/sidebar/PlSidebarTrigger.d.ts +34 -0
- package/dist/components/sidebar/PlSidebarTrigger.js +1 -0
- package/dist/components/sidebar/index.d.ts +4 -0
- package/dist/components/sidebar/index.js +1 -0
- package/dist/components/switch/PlSwitch.js +1 -1
- package/dist/components/table/PlTable.js +1 -1
- package/dist/components/text-field/PlTextField.js +1 -1
- package/dist/components/text-link/PlTextLink.d.ts +25 -2
- package/dist/components/text-link/PlTextLink.js +1 -1
- package/dist/components/toast/PlToast.js +1 -1
- package/dist/components/toggle/PlToggle.d.ts +55 -0
- package/dist/components/toggle/PlToggle.js +1 -0
- package/dist/components/toggle/PlToggleGroup.d.ts +48 -0
- package/dist/components/toggle/PlToggleGroup.js +1 -0
- package/dist/components/toggle/index.d.ts +4 -0
- package/dist/components/toggle/index.js +1 -0
- package/dist/components/tooltip/PlTooltip.js +1 -1
- package/dist/components/transfer/PlTransfer.d.ts +55 -0
- package/dist/components/transfer/PlTransfer.js +1 -0
- package/dist/components/transfer/index.d.ts +2 -0
- package/dist/components/transfer/index.js +1 -0
- package/dist/css/avatar-group.css +2 -0
- package/dist/css/color-picker.css +2 -0
- package/dist/css/command-palette.css +2 -0
- package/dist/css/fieldset.css +2 -0
- package/dist/css/footer.css +2 -0
- package/dist/css/form.css +2 -0
- package/dist/css/header.css +2 -0
- package/dist/css/menubar.css +2 -0
- package/dist/css/navigation-menu.css +2 -0
- package/dist/css/page-layout.css +2 -0
- package/dist/css/sidebar.css +2 -0
- package/dist/css/toggle.css +2 -0
- package/dist/css/transfer.css +2 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1 -1
- package/dist/internal/avatar-group.d.ts +25 -0
- package/dist/internal/avatar-group.js +2 -0
- package/dist/internal/color.d.ts +109 -0
- package/dist/internal/color.js +1 -0
- package/dist/internal/link.d.ts +18 -0
- package/dist/internal/link.js +1 -0
- package/dist/internal/page-layout.d.ts +140 -0
- package/dist/internal/page-layout.js +1 -0
- package/dist/internal/picker.js +1 -1
- package/dist/internal/search.d.ts +31 -0
- package/dist/internal/search.js +1 -0
- package/dist/internal/styles.d.ts +9 -0
- package/dist/internal/styles.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/tailwind.css +26 -0
- package/dist/tokens.css +26 -0
- package/package.json +3 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
> This package's history. The Flutter package keeps its own at [`packages/flutter/CHANGELOG.md`](https://github.com/jooy2/plass-ui/blob/main/packages/flutter/CHANGELOG.md), because the two version independently.
|
|
4
|
+
|
|
5
|
+
## 1.1.0 (2026-08-30)
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`PlAvatarGroup`.** A stack of avatars, overlapping, with the ones that did not fit as a `+n`. `size`, `shape`, `variant`, `color` and `elevation` are set once on the group rather than on every avatar — a stack whose fourth face is a size out is not a stack — and an avatar's own prop still wins, which is what marks one of them out from the rest: the person on call, the account that owns the thing. It reaches an avatar through a **context** rather than by cloning children, `internal/avatar-group.ts` following `internal/button-group.ts` one component over and for the same reason: `React.cloneElement` stops working the moment a caller wraps one of the faces in a `PlTooltip` or produces the row from a `.map()`, which is most of the time. `max` is how many faces are drawn and `total` is how many there are altogether, for the common case where the group was handed only the first five out of a hundred and twenty-eight. The one thing here that breaks a rule is the **ring**: each face carries a 2px outline in `--plass-surface`, the only opaque edge in the library, and it is not decoration — two circles of similar tone laid over each other have no boundary between them at all and the stack reads as one smeared shape, and a translucent hairline would not help because what is behind it is the other avatar. Drawn in the page's own sheet colour it reads as the _hole_ the near face is cut out of rather than as a line around anything. The avatar page's hand-rolled stack goes with it.
|
|
10
|
+
- **`PlMenubar`.** The strip of words at the top of an application — File, Edit, View — each of which opens a menu. What makes it a _bar_ rather than a row of separate menus is what happens once one is open: moving along the strip walks through the others instead of closing the one you left, and the arrow keys move between the menus as well as inside them. A `PlMenubarMenu` has no `size`, `color` or `density` of its own, because all three belong to the bar — the only place they can be set once and hold for every menu on it — and the rows inside are the same `PlMenuItem`, `PlMenuSeparator`, `PlMenuGroup` and `PlMenuSubmenu` a `PlMenu` takes, because it **is** the same menu. Two sizing decisions are its own: the trigger sits a rung **below** the control ladder at every step and takes the compact padding track even at `density="default"`, because a menu bar is a strip of words rather than a row of buttons and `File Edit View` sized as controls would make the bar taller than the toolbar it is drawn on. It draws no surface: a menu bar sits _on_ something, and a sheet under a strip that is already on a sheet is two sheets.
|
|
11
|
+
- **`PlColorPicker`.** A colour chosen by eye: a saturation square with a hue rail beside it, the arrangement every design tool has settled on because it is the one that puts every colour of a hue within a single movement of the pointer. The decision the whole component turns on is that **the panel's state is HSV and it never leaves that model.** Through RGB every shade of black is the same colour, so a picker that re-parsed its own output would snap the hue rail to red the moment the pointer reached the bottom of the square; here the model is kept and the string is derived from it, and an incoming `value` re-seeds the model only when it means something different — compared as a _colour_ rather than as a string, because `#FF0000` and `#ff0000` are the same colour written twice and a string comparison would re-seed on every render forever. `alpha` adds a third rail and the two functional notations drop their fourth argument when the colour is opaque, so a caller who never turned it on never sees `rgba(…, 1)`. The default swatches are a plain spectrum plus the greys rather than the library's six families: those are semantic roles, and a picker is asked for a colour rather than for a meaning. The tick on a chosen swatch is black or white by **relative luminance**, because a fixed white tick disappears on yellow. There is no colour library under any of it — the conversions are a new `internal/color.ts`, a hundred lines of arithmetic, which is the entire reason a component that computes colours brings nothing with it.
|
|
12
|
+
- **`PlCommandPalette`.** Everything an application can do, behind one field — the shape a keyboard-first product takes once it has more actions than a menu bar can hold. It is not a `PlMenu`, which is a short list in one place where every row is visible before you look for it, and not a `PlCombobox`, because what comes back is not a value but _something happening_. Base UI's Autocomplete owns the list and its Dialog owns the sheet, so the highlight the pointer and the arrow keys share is **one** mark rather than two, and a reader never has to work out which of two highlighted rows Enter would run. Filtering happens before the list rather than inside the primitive, which is what lets a group heading be drawn from the same array the rows come out of; each command's searchable text is folded **once per list** rather than once per comparison, because a `normalize` on every command on every keystroke is exactly the cost that makes a palette feel slow. `keywords` are matched and never drawn. `shortcut` on a row is displayed and **not bound** — the application binds it — while the one key the component does bind is its own opener, read with the same `Mod`-aware vocabulary `PlHotKeys` draws, so the cap on the screen and the key that works cannot drift apart.
|
|
13
|
+
- **`PlFieldset`.** A group of controls that answer one question together, with a name on it. It draws **no surface**, and that is the decision the component is: a group of fields is a _grouping_ and not a sheet, and the sheet already exists — put it inside a `PlCard` or a `PlBox` when one is wanted. What it owns is the legend, the gap the controls stand at, and the one thing only a real `<fieldset>` can do: `disabled` reaches every control inside it, including one a component three levels down rendered and never heard of. Two things are undone on the way: the browser's own border, padding and margin, which are nobody's design, and `min-width: min-content`, which is what makes a fieldset holding a wide table refuse to shrink. The legend is a `<div>` pointed at by `aria-labelledby` rather than a `<legend>` — Base UI's call, and the one that makes the group an ordinary flex container, because a rendered `<legend>` is lifted out of its fieldset's content box by every browser and a `gap` would put no space under it at all.
|
|
14
|
+
- **`PlForm`.** A `<form>` that knows which of its fields is wrong. On its own, a page of `PlTextField`s validates one field at a time and a failed submit leaves the reader to find the red one; what this adds is the part that has to be owned _above_ the fields — a submit collects every field's validity at once and focuses the first that failed, and `errors` puts a server's answer back on the field it belongs to rather than in a banner at the top, clearing it the moment that field changes. `validationMode` defaults to `onSubmit`, which is the only one of the three that does not tell somebody their email is wrong while they are still typing it. It is **not a form library**: there is no schema, no resolver and no field array here, and a project that has those hands the result to `errors`, which is the seam the whole component is built around.
|
|
15
|
+
|
|
16
|
+
Making that work needed one change to the fields themselves: every component that renders a `Field.Error` now renders **one either way**. It used to appear only when the caller had written an `error`, which meant a field marked invalid by the browser's own constraint validation — or by a form's `errors` — went red with nothing said. With a caller's message the box is theirs and shows unconditionally; without one it is left empty for Base UI to fill with whatever actually failed. `PlCheckbox`, `PlCombobox`, `PlNumberField`, `PlOtpField`, `PlRadioGroup`, `PlSelect`, `PlSwitch` and `PlTextField` all changed the same way.
|
|
17
|
+
|
|
18
|
+
- **`PlNavigationMenu`.** A site's navigation: a row of destinations, some of which open a panel of more of them. The difference from a `PlMenu` is what the rows _are_ — a menu holds actions, so its rows are `menuitem`s and the whole thing is a widget that traps the arrow keys; this holds links, so it is a `<nav>` full of real `<a>`s, which is what puts them in the link list, on the status bar and in a crawler's index. An item with an `href` and no children is announced as a **destination**, one with children as something that **expands**, and that distinction is the whole reason a site nav is not a menu. The row carries no surface at rest: the words are the page's own, and five bordered boxes across the top of a site is a toolbar. Base UI keeps one panel open at a time and resizes it between items rather than closing and reopening, so crossing the row reads as one surface; the panel animates opacity and its own measured size and never slides, because a panel that slid in would drag a page's worth of links across the screen. A `target` other than this tab has `noopener noreferrer` merged into whatever `rel` was asked for — the merge, not an override, because the common reason to write a `rel` by hand is `nofollow`, and spelling it as a replacement would silently take the protection off. That logic moved into a new `internal/link.ts` and `PlTextLink` now reads it too, so the library has one copy of a security default rather than two that can drift.
|
|
19
|
+
- **`PlTransfer`.** Two lists and the arrows between them: everything that could be chosen on one side, everything that has been on the other. It is the shape for a choice that is _long_ — the columns in a report, the permissions on a role, the people on a channel — where a `PlCombobox` with forty chips in its field stops being readable and a list of forty checkboxes gives no answer to "what did I actually pick"; below about a dozen options one of those two is the smaller component. The model is one sentence: **ticking is not choosing.** `value` is which side a row is on, the ticks are which rows the next press will move, and keeping them apart is what makes a press a deliberate act rather than a side effect of reading. The order of `items` is the order both lists show, so a row does not move when it is sent across and back, and moving drops the ticks on what moved and keeps the rest — a row that has arrived is not still waiting to be sent, and a row the filter was hiding was never part of that press. `searchable` folds case and combining marks through a new `internal/search.ts`, so `cafe` finds `Café`; a label that is a node rather than a string is kept, because the alternative is a row that disappears from a filter it could never satisfy.
|
|
20
|
+
- **`PlToggle`** and **`PlToggleGroup`.** A button that stays down, and a set of them that share one state. The rule the whole component is built around is that **off is neutral**: a `PlButton` at rest is an action waiting to be taken, and a toggle at rest is a state that is currently _false_, so accent ink on an unpressed toggle would be saying it was on. On, the three materials give the same two answers a `PlSegmentedButton`'s chosen segment gives — `solid` takes the family's gradient and the on-fill ink, the other two light the sheet and leave the label in the accent. The **elevation does not move with the state**, deliberately: "on" is a fact about the thing beside the toggle rather than about how far the key is off the page. The group squares off the corners that face a neighbour exactly as a `PlButtonGroup` does and publishes the _same_ context, so a toggle inside either kind of run picks the set up; it reports an **array** in both the single and the multiple case, which is the one shape that does not change type when `multiple` is turned on. Base UI owns `aria-pressed`, the controlled pair and the roving tab index — which is what makes a toolbar of eight toggles two key presses deep instead of eight.
|
|
21
|
+
- **`PlSidebar`**, and the `PlSidebarTrigger` that brings it back. A column beside the page's content, and a drawer once the window is too narrow to hold one — **two presentations of one panel**, exactly as `PlDrawer` is, because a caller should not have to swap components at a breakpoint and because the children then exist once either way rather than being rendered twice into the document for a screen reader to read twice. Which of the two is showing is a media query, and it is answered _in CSS for the first paint and in JavaScript from then on_: the markup a server sends is the column, so a narrow screen would otherwise draw a full-width sidebar and throw it away a moment later. It is a real `<aside>` — the `complementary` landmark — and it is **named by default**, because a page with two unnamed sidebars is a page offering two regions called "complementary". `resizable` writes the dragged width straight onto the element rather than into state, since nothing in the tree depends on the number except one declaration and a `setState` per pointer move would re-render every row in the panel; the handle straddles the edge rather than sitting inside it, because a one-pixel hairline is a one-pixel target. The trigger is hidden by the **same media query** the sidebar collapses at rather than by a piece of state — a button whose presence depended on `matchMedia` would be absent from the server's markup and pop into the header a moment later, on every phone, every time — and it renders nothing outside a `PlPageLayout`, where there is no sidebar it could be talking about.
|
|
22
|
+
- **`PlFooter`.** The sheet at the end of a page, and a real `<footer>` — which at the top level of a document is the `contentinfo` landmark, the region a screen reader offers as "the site's own information" and a search engine reads the copyright, the address and the site map out of. **It has no slots on purpose**, and that is the whole difference between it and `PlHeader`: a header's three regions are a fixed arrangement worth writing once, and a footer's content is four columns on one site and one line on the next — a component that guessed at the arrangement would be one every second site fights. What it decides is the sheet: the surface, the gutter, the hairline that says the document ended, and whether it stays in reach. `position` therefore defaults to `static` and not to a header's `sticky`, because a footer _is_ the end of the document; `sticky` and `fixed` are for the bar that has to stay in reach — a form's save row, a cookie notice — and inside a `PlPageLayout` the height a `fixed` one takes out of the flow is reserved rather than left sitting on the last paragraph. `divider` rules the **top** edge, which is the one that faces content.
|
|
23
|
+
- **`PlHeader`.** The bar across the top of a page, and a real `<header>` — which at the top level of a document is the `banner` landmark, and is the whole reason this is a component rather than a row of `<div>`s. Its three slots are props rather than sub-components for `PlCard`'s reason: the arrangement is fixed and what a caller decides is what goes in each — and that the middle can be centred on the bar's **own midline** is only possible because the ends are the component's to measure. `align="center"` gives both ends an equal share rather than centring the middle in the space left over, because a logo one character longer would otherwise move the navigation, which is exactly what a reader notices between two pages of the same site. `size` is a floor and not a height (`md` is 64px, a 40px control with 12px either side), so a bar whose content wraps grows and keeps its padding; `maxWidth` holds the row to `PlContainer`'s own `rem` ladder while the sheet still spans the window. It is **not a `PlToolbar` with a tag on it**: a toolbar is a row of controls anywhere on a screen and takes its height from its padding alone, and none of the floor, the measure, the brand slot or the place in a layout means anything on one. Inside a `PlPageLayout` it registers itself so a sidebar that holds its place knows how far down the window to start; outside one the registration goes nowhere and it is simply a bar.
|
|
24
|
+
- **`PlPageLayout`.** The skeleton a page is hung on: a header, a footer, one sidebar or two, and the content between them. What it is really for is the **landmarks** — a page assembled out of `<div>`s is one undifferentiated region to a screen reader and one undifferentiated blob to a search engine, and the same page built out of `<header>`, `<nav>`, `<aside>`, `<main>` and `<footer>` is a page with a table of contents. It draws no surface of its own and contributes exactly one element to the document, plus the `<main>` and the skip link that jumps to it; it draws no gutter and no measure either, because that is `PlContainer`'s job and a layout that did both would be a second spelling of one idea. The arrangement is flexbox and media queries rather than JavaScript, so it is right in the first frame the browser paints and right in a page with scripting turned off. The only thing measured is how much the two bars take out of the window, and only because a sidebar that holds its place has to start below a header whose height nobody but the header knows — written onto the root as `--p-layout-header` and `--p-layout-footer` plus an `-inset` for each, because a `sticky` bar takes nothing out of the flow while still sitting across the top and a `fixed` one has to be reserved for. Which of the two a bar is is read off its own `position` rather than plumbed through a prop.
|
|
25
|
+
- **A `PlScrollZone` answers the wheel.** A mouse has one wheel and it points the wrong way for a strip that runs across the box, and what happens when it turns over one is the browser's own business — which is the problem, because it makes the answer depend on which browser the reader is in, and a shelf that scrolls on one machine and sits still on the next is not a control. `wheel` is on by default and takes **only the vertical half** of a gesture, and only while the strip has somewhere left to go: a trackpad's two fingers, a tilt wheel and Shift held down already scroll it sideways and are the browser's, and the moment the strip reaches an end the wheel goes back to the page — a shelf that swallowed it at both ends would be a hole a reader scrolls into. The listener is a native one rather than an `onWheel`, because React attaches its own wheel listener to the root _passively_ and a `preventDefault` inside one of those does nothing but log.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **A `render` element's own `href` survives `PlTextLink`.** The component merged its `href` over whatever came back from `render`, and the documentation said to write the address once, on the `PlTextLink`. That is exactly wrong for the thing `render` exists to take: a router's `Link` does not forward an address, it _resolves_ one — a locale prefix, a base path, a typed route — and hands the anchor the result, which the merge then overwrote with the raw string. A localised app got `/pricing` on every anchor where its router had worked out `/ko/pricing`; the click still went to the right page, because the router's own handler was still attached, so what broke silently was every crawler, middle click and copied address. An element carrying an `href` is now left with it, and only an element without one — `render={<a />}` — is given the component's.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **A `PlScrollZone`'s buttons stand beside the strip rather than over it.** `buttonPlacement` now defaults to `inline`. An item sliding under a button reads as something being hidden far more often than it reads as depth, and the two scroll buttons are the one thing on a shelf that must never be mistaken for part of what is on it — so the scroller stops where the button starts and an item is cut off at its edge instead. `overlay` is unchanged and is still what a shelf of pictures wants, where the strip reaching the edge of its box is worth more than seeing the whole of the item a button came to rest on.
|
|
34
|
+
|
|
35
|
+
## 1.0.3 (2026-08-29)
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **`PlTextLink` takes a `startIcon`.** Its one icon slot was the mark _after_ the label, and it is not a general one: `icon` is about the link's destination and has an opinion, drawing the arrow leaving its box for a `newTab` link unless it is told not to. There was nowhere to put the other kind — a favicon, a file type, a lock — so a caller wrote it into `children` and paid for the gap by hand. `startIcon` is the plain node the rest of the library spells the same way: nothing is drawn unless something is put there, and it rides at the link's own `0.95em` a quarter-em from the label, the same as the mark on the other side.
|
|
40
|
+
|
|
41
|
+
- **`PlPagination` can wear the `Link` a router brings.** `getPageHref` turned every number into a real `<a href>` — which is what a crawler needs and, in a single-page app, a full document load on every press: the router never saw the click, so the whole page was fetched, parsed and booted again to change one number. The row was only usable as links by an app that navigates that way. `renderLink` is the same escape hatch `PlButton`, `PlCard` and `PlTextLink` already have, shaped for a control that needs one element per page: it is handed the page and its address, already built, so a router's own component needs no second copy of `getPageHref` inside it. `rel="prev"` and `rel="next"` are merged onto whatever comes back, so the two steppers keep saying what they are, and everything else about the row is unchanged — the current page is still a `<button>`, a modified press is still the browser's.
|
|
42
|
+
|
|
43
|
+
- **`--plass-z-portal` moves every portalled surface at once.** A modal, a drawer, a menu, a select's list, a popover, a tooltip and a toast were each painted at a hardcoded `z-50` — fifteen class strings agreeing on a number chosen for a page nobody had seen. An app arrives with a stacking ladder already built: a fixed header, a cookie bar, a video player, a game field, and `50` is a guess at where in it a dialog belongs. It is one token now, and one line in a consumer's stylesheet moves the whole set together — together being the point, since moving them apart is how a select opened inside a modal ends up behind it. Declared on a bare `:root` and repeated in neither theme block, which is the one deliberate exception to the derived-token rule and rests on it: a theme block re-declares what it carries, so a token living in one would reset an override on every nested element wearing a theme class.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **`PlTable` renders inside a React Server Component.** 1.0.1 put `'use client'` on all 75 component modules and argued that being uniform cost nothing, because outside a server-component graph the directive is inert. That is true everywhere except the one component whose own API is functions: a client boundary cannot be handed one, and every `PlTable` column is a `render` callback — so `columns` could not be written by the page that fetches the rows, which is the page a table belongs on. `Functions cannot be passed directly to Client Components` was the whole of what a consumer got, from a component that had never been touched. The directive is off it now, and the one hook it had is gone with it: naming the grid meant generating an id, so the drawn caption is `aria-hidden` and a real `<caption>` inside the `<table>` carries the same words to a screen reader. Nothing moves on screen and nothing changes for a caller on the client — a module with its own `'use client'` that imports `PlTable` gets a client component, as it does for anything else it imports, and `onRowClick` needed such a module either way. `test/package/use-client.test.ts` now asserts the rule rather than a count: a module that reaches for a hook must declare itself, and the ones deliberately kept out of the client graph are listed with what each buys. Only the decidable direction is checked — a component that builds a handler and hands it to a Base UI primitive is across the boundary too, and no regex sees that, so leaving the directive off stays a review decision.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **A `PlAccordionItem`'s title and subtitle wrap.** They were each held to one line and ellipsed, which is the right rule for a table cell and the wrong one for a fold: an accordion is most often a list of questions, and a question is a sentence. A heading that ran past the header lost its end with no tooltip, no `title` attribute and nothing to press for the rest — a fold opens its _body_, not its heading — and the narrower the screen, the more of the question went. What wrapping costs is a header two lines tall, in the one component whose entire job is to change height. `truncate` puts both back on one line for the header the old behaviour was right for: a name out of a database sitting beside a control.
|
|
52
|
+
|
|
53
|
+
### Documentation
|
|
54
|
+
|
|
55
|
+
- **The `README` opens on what Plass is rather than on a word that reads as Material.** It led with "a component library with a material rather than a theme" — _material_ meaning the stuff a surface is made of, which is not the reading anyone arriving from a Flutter or an Android background will reach for, and this library has nothing to do with Material. What is there instead is one paragraph describing the design itself: the question every surface answers, the tinted glass and the clear glass that answer it, and the fact that nothing is bevelled, embossed, or moved by a press. It is worded to serve as the library's summary wherever one is needed, and the root and Flutter `README`s carry it identically. The bullets under it are written from the caller's side — what is already decided, what is learned once, what nobody has to write — and the runtime dependency is a count rather than a name, since the count is what a reader is weighing and `package.json` has the name.
|
|
56
|
+
|
|
57
|
+
## 1.0.2 (2026-08-29)
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- **The package has a `README` on npm.** There has only ever been one in this repository and it is at the root — a directory `npm publish` does not look at from `packages/react` — so from the moment the repository became a monorepo in 1.0.0, every release has arrived on npm with nothing under the install line but the fields `package.json` fills in. `packages/react/README.md` is now the React quick start, shaped like the Flutter package's own so the two siblings read the same way: install, the stylesheet and the two Tailwind routes, the background a sheet of glass needs behind it, usage, the per-component entry points, the server-component note, dark mode, the component list and the scripts. None of it is new writing — it is what the root `README` already said, moved to where the reader actually is. Links that leave the package are absolute, because npm resolves a relative one against the repository root rather than against `packages/react`.
|
|
62
|
+
- **The npm page's Repository link points at `packages/react`.** `repository.directory` is the field that says a package is one folder of a monorepo rather than the whole of it. Without it npm sends a reader to the repository root — which is now a map to two packages rather than this one's documentation — and resolves a relative link in the published `README` from there as well.
|
|
63
|
+
|
|
64
|
+
### Documentation
|
|
65
|
+
|
|
66
|
+
- **The root `README` is a map to both packages rather than the React quick start with a Flutter paragraph in it.** What left is everything a React consumer needs and a repository visitor does not. What is there instead belongs to neither half alone: a packages table carrying both registries, both quick starts and the fact that the two version independently, the shortest possible install and first component for each, a table of what genuinely differs between them — setup, runtime dependencies, tokens, dark mode, handler names — and the background caveat written twice, once in CSS and once in Dart, because it is the one setup step neither package can perform for the caller and the fastest way to conclude the glass is broken. The claim that `0.0.1` is a preview went with it: both packages are past `1.0.0`.
|
|
67
|
+
|
|
68
|
+
### Notes
|
|
69
|
+
|
|
70
|
+
- Documentation and packaging only. No component, type or stylesheet changed.
|
|
71
|
+
|
|
72
|
+
## 1.0.1 (2026-08-29)
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
|
|
76
|
+
- **`dist` is minified again, and the annotations survive it.** Terser strips `/* @__PURE__ */` unless it is asked not to, which is what made minifying a library a losing trade in 1.0.0 — but `format.preserve_annotations` re-emits every annotation it reads, and `keep_fnames` on both `compress` and `mangle` keeps the inner `function PlButton` that React DevTools names a `forwardRef` component by. All 189 annotations reach `dist`, and `scripts/minify.mjs` fails the build rather than the tarball if one of them does not, so the tree shaking 1.0.0 bought is not being spent here: the package is 222 kB packed and 785 kB installed rather than 338 kB and 1.21 MB, while every scenario `npm run size` measures moves by less than 0.3% — a consumer's own minifier had already done that work.
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- **Every component ships `'use client'`, so the package works inside a React Server Component graph.** Next.js's App Router is where nearly everyone meets one, and until now `import { PlButton } from 'plass-ui'` in a `page.tsx` failed at `i.createContext is not a function` — a build error in somebody else's app, thrown by a package that builds, tests and documents cleanly here. All 75 component modules carry the directive and the barrels deliberately do not: the boundary is per-module, and a barrel that carried it would drag the library across it whether or not the component being imported needed to be. It is on every component rather than on the handful that would strictly need it today, because outside a server-component graph a module-level directive is inert — Vite, Remix, React Router, Astro and a plain `tsc` build all ignore it — so the cost of being uniform is nothing and the cost of guessing is a component that works everywhere except the one framework most people are using. Nothing else changes for a consumer: no `transpilePackages`, no `next.config` entry, no provider. What the directive cannot do is carry a function across the server boundary, which is React's rule for every client component and not this library's — a file passing `onClick`, `onValueChange` or `render` needs its own `'use client'`. Two things guard it, because nothing else in this repository would notice one going missing: `scripts/minify.mjs` sets `compress.directives: false`, since terser's default is to strip every directive it does not recognise and `'use client'` is not one it recognises, and fails the build if a file that arrived with the directive leaves without it; `test/package/use-client.test.ts` asserts the directive on every component module and its absence from every barrel.
|
|
81
|
+
|
|
82
|
+
## 1.0.0 (2026-08-29)
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
|
|
86
|
+
- **A short `PlSpoiler` no longer clips its own Reveal button.** The cover was absolutely positioned over a box whose height the content alone decided, so a one-line spoiler was shorter than the button it was asking somebody to press. Content and cover now share one grid cell: the row is as tall as whichever of the two needs more, and they stretch to match.
|
|
87
|
+
- **A floating bottom bar shows which destination you are on, and slides the key there.** `PlFloatingBottomNavigation` painted its current disc with `var(--p-fill)`, `var(--p-on-solid)` and `var(--p-lift)` while taking `surfaceSlots` — the _container_ slot set, which deliberately carries none of the three. All three resolved to nothing, so the gradient was a `background-image` the browser dropped and the current destination looked exactly like the four beside it. It reads `controlSlots` now, and the fill has moved off the disc onto a key that **travels**: one element measured off whichever disc is current and animated between them, the way a `PlSegmentedButton`'s tile is. Nothing is transformed, so no glyph in the row is resampled while it moves.
|
|
88
|
+
- **The package now loads in Node.** `tsc` copies a relative specifier through exactly as written, and every import in the source was extensionless — so `dist/index.js` said `export * from './types'`, which every bundler resolves and Node's ESM loader does not. `import { PlButton } from 'plass-ui'` threw `ERR_MODULE_NOT_FOUND` in any environment that is not a bundler: a server render, a React Server Component, a Vitest run that does not inline the dependency. Every relative specifier in the package now carries its `.js`, and `npm run size` loads all 47 entry points through Node's own resolver so this cannot come back.
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- **`dist` is no longer minified, and that is what makes it smaller.** Terser was running over every emitted file, and terser strips `/* @__PURE__ */` — the annotation that tells a bundler a top-level call is safe to drop. Without it, esbuild keeps every `React.forwardRef`, every `React.createContext` and every `[...].join(' ')` in a file, used or not, because a call it cannot prove pure is a call it must assume does something. So a component that shared a file with another shipped both. Those annotations are now written in the source and survive to `dist`, and the minification that was destroying them turns out to be worth 0–1% gzipped once the consumer's own minifier has run — which it always has.
|
|
93
|
+
- **`severityIcons` is `severityIcon()`.** Six drawings in a table built at module load is a table a bundler keeps whole or not at all, and it was reaching every component that touched `internal/icons`: a bundle whose only Plass component was `PlButton` still carried the warning triangle. A function declaration is dropped outright by anything that does not call it.
|
|
94
|
+
- **A table stops being somebody else's table.** `PlTable` now writes its cell borders, and the `<table>`'s own `display`, `width`, `margin` and `border-collapse`, as inline styles. Padding and alignment were already inline for the specificity reason; the borders were not, so on any page with prose styles — VitePress, Tailwind Typography, every CSS framework — a host's `td { border: 1px solid }` drew a full grid of cell rules the design never asked for, `table { display: block }` stopped the grid filling its sheet, and `table { margin: 20px 0 }` pushed it off the corner of the pane. The band behind the column names is gone with it: the header is muted, semibold text over a firmer rule, and rows are scored with a hairline. A filled strip across the top of a grid is the fastest way to make data look like chrome. `stickyHeader` keeps a fill, because there the fill is doing a job — rows pass directly underneath it.
|
|
95
|
+
- **Three tokens for the marks a sheet makes on itself**: `--plass-divider`, `--plass-stripe` and `--plass-track`. `--plass-glass-line` is white light on a cut edge and reads because the page wash is behind it; turned inward — between two rows, under a switch's thumb, across every other row of a table — it is white on white, which is why a light-mode `PlCard`, `PlList` or `PlTable` had no visible structure of its own. All three are neutral inks. `PlCard`'s dividers, `PlList`'s rules, `PlAccordion`'s folds and `PlTable`'s rows now share one line.
|
|
96
|
+
- **A switch you can find when it is off.** The off track is `--plass-track` rather than the glass at its most opaque, and it has lost its inset shadow, its hairline and the drop shadow under the thumb: a white pill with a white thumb in it is invisible on a light page, and where it _was_ visible — in the dark — a recessed slot under a domed thumb was a moulded rocker, which is the one picture this design language exists not to draw. The thumb is white in both themes. Dropping the track's border also squared the thumb's travel, which had been 2px from the left of the track and 4px from the right at every size.
|
|
97
|
+
- **The focus ring is flush.** `outline-offset` is `0` on every control in both packages, and the strays at `1px` and `2px` are gone. A ring held off a control that draws an edge of its own reads as three concentric rectangles round one object. The exception is a control something else clips — a tab, a segment, a table row, an accordion header on a scored pane — which takes `-2px` so the ring is not sliced off at the container's edge. Every one of them now comes from `focusRingClasses` / `focusRingInsetClasses` rather than being written out per component.
|
|
98
|
+
- **A field is drawn with the same hairline as the tick beside it.** `--plass-border` rather than `--plass-glass-line`, which a `PlCheckbox`, a `PlRadio` and a `PlTabs` rail already used for the reason it applies here too: a field is very often on a card rather than on the page wash, and a white line round a near-white box on a white card is a field whose shape cannot be seen.
|
|
99
|
+
- **A radio's dot lands on whole pixels.** The diameters were already whole numbers; the _offsets_ were not — a 7px disc inside an 18px ring with a 1px edge sits 4.5px from every side, and a circle antialiased at half coverage on all four sides reads as though it has drifted up and to the left. Every dot now has the same parity as the ring's content box, and the line box a tick shares with its label is a whole number too, which is what stops the ring itself from starting at a fraction of a pixel.
|
|
100
|
+
- **A slider's rail is the same groove a switch's off state is**, and no longer the well. A field is a box you look into; a rail is a line you look along, and the part of it that matters is the part with nothing on it — which a white-on-white groove does not have.
|
|
101
|
+
- **An accordion's body has padding above it.** The header's padding buys room around the _title_; a body starting at the header band's bottom edge put its first line half a leading under the subtitle, so the heading and the paragraph explaining it read as one run of text broken by a colour change.
|
|
102
|
+
- **The repository is a monorepo.** What was the root is now `packages/react`, and it is installed and run from there — `cd packages/react && npm install`. The documentation site has its own `package.json` and runs from `docs/`. Nothing about the published npm package changed.
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
|
|
106
|
+
- **`PlAnimateTyping`.** Text appearing one character at a time, and it costs both kinds of reader nothing. The whole string is in the document from the first frame in a clipped box, which a screen reader reads **once** rather than being made to sit through the performance; what animates is a visible copy that is `aria-hidden`, and the box is not laid out from the characters that have arrived, so the text around it does not reflow on every frame. The advance is by **grapheme** and not by code point — `Intl.Segmenter` knows where the boundaries are, and a typewriter that advanced by code points would spend four frames assembling an emoji out of parts that mean nothing on their own. `speed` is characters per second and is the default unit, because a long paragraph and a short one should be typed at the same pace rather than in the same time; `duration`, if given, is the time for the whole string and overrides it. `repeat`, `hold` and `erase` are what make it a loop — type, hold, delete, type again — and without `erase` a repeat clears in one frame, which is right for a line being replaced rather than rewritten. Only text is typed: an element among the children contributes its text and nothing about its markup, because there is no honest way to reveal half of a link.
|
|
107
|
+
- **`PlAnimateHeadline`.** One line replacing the one above it, on a timer. Every line sits in the **same grid cell**, so the box is as tall as the longest of them from the first frame and never resizes as the reel turns — which is the whole difficulty with this effect, and the reason the lines that are not showing keep their space with `visibility` rather than being taken out of the layout. It is deliberately **not a ticker**: a line comes up, it stops, and it is held long enough to read, with `interval` counted from the moment a line _arrives_ rather than from the start of the cycle, so raising `duration` does not quietly eat the reading time. Passing `index` makes it controlled and stops the internal timer outright — a controlled headline is somebody else's clock, and a second one running underneath it would fight for the same state. What it is not for is content a reader has to see: there is no guarantee anyone is looking during the two seconds a line is up, and a screen reader is given whichever line happens to be showing rather than the set.
|
|
108
|
+
- **`PlAnimateMarquee`.** Content scrolling steadily past, forever. The content is laid down **twice** and each copy travels exactly its own length plus the gap, so the moment the first copy has left the second is standing precisely where it began — no seam, no jump and no frame where the strip is empty. None of that depends on measuring anything: a percentage `translate` resolves against the element's own box. What _is_ measured is the **speed**, and that is the prop: `speed` is pixels per second rather than a duration, so a strip of four logos and a strip of forty move at a reader's pace instead of the long one becoming a blur, and the gap is read back off the computed style rather than parsed out of the prop, because `'2rem'` is only a number once a font size has resolved. `pauseOnHover` is on by default and is not decoration — content moving past a pointer cannot be clicked reliably. Only the first copy is read out; the rest carry `aria-hidden`.
|
|
109
|
+
- **`PlAnimateLighting`.** A light travelling around the outside of something, and the only way this library has of saying _here_ without also saying _and it moved_. The light is **behind** the content rather than on it — `z-index: -1` inside an isolated stacking context — so what a reader sees is a glow escaping from under the edges, nothing inside is altered or overlaid, and it works on a `PlCard` or a `PlButton` without touching how either is drawn. The arc **turns between the two ends of the family** as it travels, which is the same rule every filled surface follows: a flat coloured arc would be the one place in the library where a coloured surface is paint. What actually moves is the `from` angle of a conic gradient, which is only animatable because the angle is a registered custom property with a type — rotating the pseudo-element instead swings its corners out past the element on every quarter turn. `size` has to agree with the radius of what is inside it, because the glow follows the wrapper's own corners.
|
|
110
|
+
- **`PlAnimateAppear`.** A list of things settling into place one after another, and the effect belongs to the **set** rather than to any one item: each child takes the same short drift and fade, held back by its position, so a reader's eye is walked down the list in the order it should be read. The animation is written **onto the children themselves** rather than onto wrappers around them — a row of `<li>`s stays a row of `<li>`s, a grid's cells stay its direct children, and nothing about the layout changes because the list is being animated. Whatever class and style a child already had are kept beside the ones this adds; only a bare string, which has no element to write onto, gets a `<span>`. The stagger counts **children and not leaves**, so eight children are eight steps and one child holding eight things is one step — which is also how to opt part of a list out. `distance` is deliberately short: this is a settling, not an entrance from off screen, and a long travel over a list of eight turns the whole block into something moving.
|
|
111
|
+
- **`PlAnimateBlink`.** Content pulsing between full opacity and a floor, on a **symmetric** cycle — full, faint, full — so however many times it runs it ends where it started. A blink that finished halfway would leave the element permanently half drawn, which reads as a rendering fault rather than as an effect that ended. `repeat` defaults to `'infinite'` here and to `1` on every other effect, because a single blink is a flicker and nobody asks for a flicker. `min` is the floor, and it is worth raising for anything that has to stay readable while it pulses. Two things it is honest about: something that never stops moving in the corner of a page being read is the one kind of motion the rest of this library refuses, and a reader with a reduced-motion preference sees none of it — so `min` is a dimming and never the only thing carrying the message.
|
|
112
|
+
- **`PlAnimateRotate`.** Content turning about a point, with **two angles rather than one** — which is what lets a single component be both effects a rotation is ever used for. `from` alone is an arrival: something swings into place and stops. `from={0} to={360}` with `repeat="infinite"` and `easing="linear"` is a spin that never lands, which is what a badge, a loading mark or a decorative glyph wants, and it needs `fade={false}` because a fade that repeats reads as flickering. `origin` makes the difference between a wheel and a hinge. It is **not for text**: a rotated word is resampled along its whole length, which is exactly what the rule against transforming a control exists to prevent — rotation is the one movement the design language allows on a _glyph_ without argument, and a chevron is turned rather than redrawn all over the library.
|
|
113
|
+
- **`PlAnimateSlide`.** Content travelling in from one edge, and the default distance is the element's **own size** — so it starts exactly out of frame and arrives without ever having been half drawn somewhere it does not belong. Put it in a box with `overflow: hidden` and the effect is a panel appearing from behind that box's edge; give it twenty pixels instead and it is a different gesture altogether, a nudge that says something changed rather than an entrance. `from` is physical — `top`, `right`, `bottom`, `left` — as `PlassSide` is everywhere in the library, because a panel coming down from the top comes from the top in every writing direction. Nothing around it reflows while it runs: this is a `translate` and not a change of layout.
|
|
114
|
+
- **`PlAnimateZoom`.** Content arriving from the middle of where it will end up — the same arithmetic as `PlAnimateGrow` at more than twice the distance, and always about the centre. That is the whole difference: a grow unfolds _from_ somewhere, a zoom comes at you, which makes it the effect for the one thing on a screen that is meant to interrupt — a confirmation, a result, a figure that has just landed. There is deliberately **no `origin`**: a zoom anchored to a corner is a grow, and the library does not offer two spellings of one idea. Above `1` it arrives oversized and settles back, which is the other reading of the same gesture.
|
|
115
|
+
- **`PlAnimateGrow`.** Content unfolding from a point. What separates it from a zoom is `origin` and how far it travels: a grow starts close to its final size and can be anchored to any edge, so it reads as something opening out of the thing beside it — a panel out of a toolbar, a card out of the row it belongs to. `transform-origin` governs the standalone `scale` property too, which is what lets the effect stay off the `transform` shorthand entirely and a caller's own transform on the same element survive. Short travel is not timidity either: a sheet growing from `0.8` stays recognisably the same sheet the whole way, and the blur behind it is never asked to resolve a surface a fifth of the size it is about to be. It shares one `@keyframes` with `PlAnimateZoom` — they are the same arithmetic at two strengths, and a second identical keyframe would only be a second place to fix a bug.
|
|
116
|
+
- **`PlAnimateFade`, and the machinery the whole `PlAnimate*` set runs on.** Content arriving or leaving on opacity alone — the plainest effect there is, and the one that suits the material: a Plass sheet is defined by what shows through it, and the glass ladder is itself built out of opacity. Everything under it is one idea. An effect is a **single `@keyframes`** that runs from a state written entirely in `--p-anim-*` custom properties to the element's natural one, so `internal/animate.ts` never generates CSS — it fills slots, exactly as `controlSlots()` does for colour, and for the same reason: Tailwind only sees class names that appear literally in the source. Because the from-state is the keyframe rather than a second class, `mode="out"` is `animation-direction: reverse` and nothing else, and it is **held at the end** — a faded-out element stays faded out rather than snapping back when the run finishes. Waiting is `animation-play-state: paused` rather than a second class, which is what lets a `trigger="visible"` element sit on its own first frame instead of being fully drawn and then blinked out the moment it scrolls in. And the ten settings every `PlAnimate*` takes — `duration`, `delay`, `easing`, `repeat`, `alternate`, `paused`, `trigger`, `play`, `once`, `threshold` — are one interface in `types.ts`, so a `delay` of 200 means the same thing on a fade as on a marquee. This is also where the rule against transforms is at its narrowest: a _control_ still never moves, and what moves here is content a caller asked to have moved, on the independent `translate` / `scale` / `rotate` properties rather than the shorthand, so a caller's own transform on the same element survives.
|
|
117
|
+
- **`PlTable` takes a `maxHeight`, and `stickyHeader` finally has something to stick in.** The cap is on the **grid**: past it the rows scroll inside the sheet rather than the page growing under them, and a pinned header keeps the column names at the top of what scrolls. `stickyHeader` on its own still does nothing, and now says so — a `position: sticky` header in a box as tall as its content has nowhere to stick. Two things had to move for it: the `caption` is a heading above the scrolling box that the `<table>` points at with `aria-labelledby`, rather than a `<caption>` that would slide away and take the table's accessible name with it; and a pinned header's rule is an inset shadow rather than a border, because `border-collapse: collapse` hands a cell's borders to the _table's_ border grid and that grid does not travel with a sticky cell.
|
|
118
|
+
- **`PlDateTimePicker`.** A day and a time in one popup — not a date picker that grew a clock and not a time picker that grew a calendar. The two panels sit side by side at exactly the same height, seven rows of cells each, because the calendar's grid and the clock's columns read the same `--p-cell` ladder: the popup is one rectangle rather than two of different sizes pushed together. The bounds do more work here than anywhere else, and it is the one place this parts company with `PlDatePicker`: `minDate` is read at **full precision**, so a minimum of 09:30 on the 27th leaves the 27th selectable in the calendar and greys out the morning in the clock. That is what a "not before now" rule needs, and a day-granular check can only block the whole of today or allow this morning. Picking a day leaves the clock alone and picking an hour leaves the day alone, because nobody reads a popup in the order it was written. The trigger wears the calendar glyph alone: a control cannot say two things at once. `name` submits a local `YYYY-MM-DDTHH:MM`.
|
|
119
|
+
- **`PlTimePicker`.** A time of day, chosen from **columns** rather than a dial: "half past nine" is two glances at two columns and "any time at all, on the hour" is a column you never touch, while a clock face is prettier, answers neither faster, and needs a `transform` to read. The bounds are checked at the granularity of the column being drawn, which is what separates a working time picker from a frustrating one: with a `minTime` of 09:30 the hour `9` stays available — it _contains_ allowed minutes — and the minute column is where `00` through `25` grey out. Comparing the whole candidate instead hides the 9 and makes half past nine unreachable. `closeOnSelect` is **false** here and true on `PlDatePicker`, because a day is one answer and a time is two; the footer therefore carries a `Done`. The value is a `Date` rather than a string or a count of minutes, and `referenceDate` — the day a bare time is written onto — is held still for as long as the picker is mounted, so a popup left open across midnight does not move the value onto a new day. The chosen row is brought into view by setting `scrollTop` on its own column, never `scrollIntoView`, which on the frame the popup opens would scroll the whole page.
|
|
120
|
+
- **`PlDateRangePicker`.** A span between two days: `PlDatePicker` with a second end, two months side by side because a range that crosses a month boundary is the ordinary case. The two panels are **one calendar in two halves** — the left has no forward stepper, the right has no back stepper, and either header moves both — and they draw no outside days, which is not taste: with both showing six full weeks the 1st of August would appear twice, and two cells with the same name in one popup is ambiguous to a pointer and broken to a screen reader. The band between the ends is drawn as the pointer moves, **before the second click lands**, and that preview is the whole affordance: without it the first click has no visible consequence. Clicking backwards is the same range typed in the other order, not a mistake to reject. The value is one object rather than a tuple or two props — a range is chosen, cleared and validated as a whole — and the half-finished state lives inside the component, so a controlled caller is never handed a range mid-gesture. `presets` takes a function for anything that depends on today. With `name`, two hidden inputs of one name arrive as `FormData.getAll`.
|
|
121
|
+
- **`PlDatePicker`.** One day, chosen from a calendar, on a `PlTextField`'s shell wearing a calendar glyph. It adds **nothing to your dependency tree and no translation files**: everything it does is either `Date` arithmetic or `Intl`, so `locale` is a BCP 47 tag and from it the platform supplies the month and weekday names, AM/PM, which day the week starts on, the order of the header's two buttons and the trigger's own format — a project shipping in twelve languages pays nothing for eleven of them. The only strings left are the ones on the picker's own buttons, and those are one `labels` object with English defaults. The header is what the component is _for_: the month name and the year are each a button that opens a grid of its own, so any month of the year on screen is two clicks and any year at all is three, and all three views are the same width **and** height so switching never resizes the popup. The day grid is always six weeks, for the reason `PlPagination` pins its slot count. There is deliberately **no typing into the trigger** — parsing a date out of free text cannot be done honestly without a date library. A blocked day carries `aria-disabled` rather than the attribute, so it keeps its place in the arrow-key path. With `name` the hidden input carries a **local** `YYYY-MM-DD`, never `toISOString()`.
|
|
122
|
+
- **`PlCombobox`.** A field you can type into and also choose from. The shell is a `PlTextField`'s wearing a chevron, exactly as `PlSelect`'s trigger is — the three have to be indistinguishable in a form or the form looks assembled rather than designed. What is different is what the text does: it filters the list, and — unless `allowCustom` is off — it can become the value itself, offered as **its own row at the end of the list** rather than committed silently on blur. That row is a real option and not a special case in the key handling, so a click, Enter and the arrow keys all reach it the way every other row is reached. With `multiple` the chosen values become `PlChip`s inside the field and the input goes on filtering after each one, so a set of tags is built without the field ever closing — and the field's height becomes a _minimum_, because chips wrap. Each chip's × is named after its chip. Base UI owns the filtering and its collator, the popup, the `combobox`/`listbox` wiring, arrow-key navigation across both the list and the chips, and the hidden input that makes it submit.
|
|
123
|
+
- **`PlProgressBox`.** A row of small glass plates that light up — the third shape, and the one that is about the material rather than about the quantity. A bar and a ring say _how much of it is done_; a row of plates says _this is working_, in the library's own vocabulary — the same groove, the same corner, the same gradient — which makes it the right one for a loading state inside a Plass surface, where a foreign grey spinner would look borrowed. With a value the plates fill in order and the leading one partially, so four plates at 30% are one full plate and a fifth of the next rather than a quarter rounded off; each plate is a groove of its own for exactly that reason. Indeterminate, they cycle, and what cycles is the fill's **opacity** and never its paint: `background-image` has no interpolation between a gradient and nothing, so a plate that swapped its background would snap rather than light. They never move.
|
|
124
|
+
- **`PlProgressCircular`.** A ring that fills, for where there is no room for a bar — a table cell, the end of a line. It takes `PlProgressLinear`'s props unchanged except for `size`, which measures diameter here rather than thickness, and that is the whole claim the indicators make: one component in three shapes. The arc is the family's **gradient** and not a flat colour, which costs a `<linearGradient>` of its own — an SVG stroke cannot be given a CSS one — and is worth it, because a flat ring beside a swept bar is two materials for one idea. The value and the label sit _beside_ the ring rather than inside it: a number in the middle of a dial works at two of the five sizes, and at `xs` the ring is fourteen pixels across. Indeterminate, it draws a fixed quarter-arc and turns, which is the same exception the button's spinner already has.
|
|
125
|
+
- **`PlProgressLinear`.** A bar that fills — the one indicator that can show _how much_ is left at a glance, because length is the one quantity a reader can compare without counting. The groove is `--plass-track`, the same neutral ink a `PlSlider`'s rail and a `PlSwitch`'s off state are cut in, so a form holding all three is made of one material rather than three; the segment over it is the family's gradient, which is exactly what the button that submits the form is made of. There is **no `variant`, no `density` and no `elevation`**: an indicator is one material, it has nothing to pad, and it is cut into the surface it sits on the way a groove is — and a groove does not float. `value` defaults to `null`, which is the indeterminate case and makes the bar sweep rather than sit empty, because an empty bar is a claim that no progress has been made. Both ends are fully rounded, and that is the one place the rule against pills does not apply: at six pixels tall there is no flat run left to protect.
|
|
126
|
+
- **`PlButtonGroup`.** A run of buttons that belong together. Two things happen and only one of them is visual: the corners that face a neighbour are squared off, and `variant`, `size`, `color`, `density`, `elevation` and `disabled` are stated **once for the set** rather than repeated on every button — a group where one button is a size out is the failure it exists to prevent. The axes have no defaults of their own, so an axis the group does not state is one each button falls back to its own default on, and a button that states one itself still wins. Inheritance travels by context rather than by `cloneElement`, so it reaches a button wrapped in a tooltip, a fragment or a `.map()`. `glass` is the only variant that overlaps its keys by a pixel, because it is the only one that draws an edge and two hairlines meeting is a seam twice the weight of every other line on the page. The buttons stay real `PlButton`s: this is not a segmented control, it has no value and it manages no selection — that is `PlSegmentedButton`, which carries the roving focus and the `radiogroup` semantics that go with it.
|
|
127
|
+
- **`PlToolbar`.** A bar of controls — an application header, a page's action row, the strip along the bottom of an editor. Three slots and a row: `start` and `end` are pinned to their ends and `children` takes what is left, which is the arrangement every toolbar has ever had. It takes **no height**: a toolbar is as tall as the controls in it plus the `size`/`density` padding every other surface uses, so `density="compact"` gives the dense bar without a second prop meaning the same thing. And it has **no `role="toolbar"`** — that role is a promise about keyboard behaviour, and a bar that claims it without implementing it is worse for a keyboard reader than one that never claimed anything.
|
|
128
|
+
- **`PlSpoiler`.** Content that is covered until somebody asks for it. The cover is a **blur** rather than a hidden box, and that is the whole design: a reader can see that there is something there and roughly how much of it, and cannot read it by accident. Blur alone is not cover — it leaves a paragraph's colour and rhythm and a blurred face is still a face — so a wash of the page's own surface goes over the top, which also gives the button something to stand on. While it is covered the content is `inert`: not tabbable, not readable by a screen reader, and not selectable, because a spoiler that could be defeated by Ctrl-A is not one.
|
|
129
|
+
- **`PlPopover`.** A sheet that opens beside the thing that opened it — the middle of the three floating surfaces. A `PlTooltip` is a note about something else and nothing in it can be reached; a `PlModal` takes the page away until it is answered; a popover stays up until it is dismissed, can be entered with the pointer or the keyboard, and leaves the page behind it working. That last part is what `modal={false}` says by default, and it is what makes a rename or a filter belong here rather than in a modal that would have taken the page to ask one question. No `variant` and no `elevation`: a popup that had to be asked for has already answered what those ask.
|
|
130
|
+
- **`PlPill`.** A floating lozenge holding a small amount of live information — a recording that is running, an upload that is climbing. It is the one **stadium** in the library, and the exception the radius rule is drawn against: every control is held short of the 50% that would make it a pill because the flat run along its edge is what reads as a sheet with the corners cut, and this is not a sheet lying on the page. The radius is pinned to the _row_ rather than written as `rounded-full`, so a pill that has grown a second line keeps the corner it always had. `details` opens to a measured height with nothing transformed.
|
|
131
|
+
- **`PlDrawer`.** A panel attached to one edge of the window, and two things in one component because they are the same panel: `overlay` is the drawer you open — a scrim, a focus trap, Escape — and `inline` is the drawer that is simply part of the page. They are not two components precisely so that a sidebar becoming a hamburger at a breakpoint is one prop. There is no `variant` and no `elevation`: the three materials answer how much a surface asserts itself against the page, and a panel that has taken an edge of the window has answered it. Nothing slides — a drawer that slid in would drag its own text across the screen, which is the case the no-transform rule was written for.
|
|
132
|
+
- **`PlCollapsible`.** One section that folds, standing on its own — the same fold a `PlAccordion` is a set of, with nothing to coordinate with, so what it needs is an `open` of its own rather than a place in somebody's list. The panel's height is animated and that is not an exception to the no-transform rule: nothing is transformed, no text is resampled, and the panel is a window opening onto content that does not move. `hiddenUntilFound` is the one worth knowing about — a closed fold the browser's own find-in-page can search _and open_.
|
|
133
|
+
- **`PlCarousel`.** A strip of slides, one of which is in view. It is a scroll container with snap points and nothing else, which is where everything good about it comes from: swiping and trackpad dragging are the browser's own, the strip runs the other way under RTL without being told, and nothing is transformed — so the no-transform rule holds here for free rather than by exception. `autoPlay` is off by default and stays off under reduced motion, pauses on hover and on focus anywhere inside, stops in a background tab, and silences its own live region while it runs.
|
|
134
|
+
- **`PlBox`.** The plainest surface in the library: a sheet of glass with content on it, and nothing else claimed. Everything structural belongs to `PlCard`, which is a box with those sections laid out on it — this is what most of a screen actually groups, which has no heading at all. `size` is the one place in the library where the word means something other than a height: a box is as tall as what it holds and its children bring their own typography, so `size` is the size of the _sheet_ — its radius and its padding.
|
|
135
|
+
- **`PlScrollZone`.** A strip of anything — cards, chips, avatars, thumbnails — laid out in one direction and scrolled in it, in as many `lines` as you ask for. The mechanism is an ordinary scroll container and everything on top of it is a way of driving one, so swiping, two-finger dragging, the wheel, the arrow keys and the scrollbar are all the browser's own and none of them is intercepted. Nothing is transformed: a scroll offset needs no exception to the no-transform rule, and it is also what makes the strip run the other way under RTL without being told. The two buttons are real `PlIconButton`s with real names, and in `hold` mode they answer Enter and Space the same way they answer a press.
|
|
136
|
+
- **An entry point per component**, for the builds that cannot take a barrel apart — and for server rendering, where the barrel is not a bundling question at all but 63 modules and their dependencies loaded before the first component is used. `import { PlButton } from 'plass-ui/button'` bundles to the same bytes as the barrel does (5.0 kB gzipped either way, esbuild), and loads in 14 ms against the barrel's 230 ms under Node.
|
|
137
|
+
- **The Tailwind scan, published in pieces.** `plass-ui/tailwind.css` registers all 63 components, because Tailwind scans files rather than imports and nothing in a build connects `import { PlButton }` to the classes `PlSelect.js` spells out. `plass-ui/css/base.css` plus one `plass-ui/css/<component>.css` per component registers only what a project uses — about 5 kB gzipped smaller for a handful of components, and still one Tailwind pass, so the utilities keep Tailwind's own order. `plass-ui/tokens.css` is the token sheet with no scan of its own, for a project that would rather write the `@source` lines itself.
|
|
138
|
+
- **A bundle-size budget**, `npm run size`. It bundles `dist/` for real against fixed scenarios — one component, five, all 63 — with React external and `@base-ui/react` counted as ours, and fails the build when a change makes a consumer's bundle bigger. `bundlephobia` and `npm pack` cannot see a tree-shaking regression; a package can double a consumer's bundle without its tarball changing by a byte.
|
|
139
|
+
- **A Flutter package**, `plass_ui`, under `packages/flutter`. Same design language, same vocabulary, same numbers, and the same library component for component. See its own changelog for what each of them had to say differently in a language with no CSS.
|
|
140
|
+
- **A framework switch on the documentation site**, at the top of the sidebar. It changes the code, the examples and the props table on every page that has something to say for both, and the Flutter previews are the real package compiled for the web rather than a stand-in.
|
|
141
|
+
|
|
142
|
+
## 0.0.1 (2026-08-24)
|
|
143
|
+
|
|
144
|
+
The first release, and a preview rather than a product. Two components ship; what is actually being released is the shape everything after them will be poured into — the prop vocabulary, the token sheet, the build, the test setup and the documentation site.
|
|
145
|
+
|
|
146
|
+
### Added
|
|
147
|
+
|
|
148
|
+
- **The design language.** A Plass surface is **a key of tinted glass resting on a clear sheet**, and every surface answers one question: is this pressed, or does it hold something? A thing that is pressed is tinted glass — a two-stop gradient at 135° whose ends are the two ends of a colour family **at one lightness**, a drop shadow **tinted with that family**, and a bloom of light that follows the pointer across it. A thing that holds something is clear glass — translucent at one of three opacities, `blur(22px) saturate(160%)`, a white hairline round it, and never dyed, because a sheet holds other people's content and that content arrives with its own colours.
|
|
149
|
+
- **A gradient that turns, not one that shades, and no highlight over the top of it.** primary runs indigo to azure, danger vermilion to rose, success green to teal. A gradient that darkens toward one corner is a moulded object catching a lamp and needs a specular highlight to finish the illusion — which is what makes a filled control read as lacquer. A gradient that turns is a pane of tinted glass and needs nothing else, so a `solid` control carries no inset edge at all; the hairline belongs to `glass`, which has a real cut edge.
|
|
150
|
+
- **`Button`.** `variant` (`solid` · `glass` · `ghost`), `size`, `color`, `density` and `elevation` off the shared vocabulary, plus `startIcon`, `endIcon`, `loading`, `readOnly`, `disabled`, `fullWidth` and `render`. A `glass` button wears the family in its **label** rather than in its sheet, which is what makes `color="secondary"` the quiet neutral button instead of a fourth variant nobody would remember the name of.
|
|
151
|
+
- **`elevation` defaults to `1` on Button**, against the `0` a flatter language would use: a control rests _on_ the sheet rather than lying flush with it. Hover adds a level and pressing removes one, so a default button presses down onto the sheet under the finger and a raised one comes back to where it was. The ladder itself is neutral, wide and faint — most of a control's shadow is the tint below.
|
|
152
|
+
- **`TextField`.** Single- or multi-line, with `label`, `description` and `error` as part of the component rather than three elements wired together by the caller — Base UI's `Field` points the label at the control and puts both messages in its `aria-describedby`. `error` carries a message _and_ turns the field invalid, which re-points the whole slot family at `danger` so the hairline, the ring, the caret and the message all turn over together; `invalid` and `invalid={false}` are the two escape hatches for a form library that owns the validity.
|
|
153
|
+
- **`solid` means the deepest glass on a field, not a tinted pane.** A gradient under a caret, a text selection and a placeholder is not legible, so a `solid` TextField is the **well** — `--plass-well`, the one shadow in the library that points inward. Same word, one rule underneath it: `solid` is the heaviest thing a variant can be while still doing its job.
|
|
154
|
+
- **Six colour families, three hand-picked values each.** `--plass-{color}-solid`, `-solid-to` and `-on-solid` are declared once and are the same in both themes — a pane of blue glass is the same pane in a dark room — and everything a component reads (`-fill`, `-tint`, `-soft`, `-line`, `-ring`) is computed from them with `color-mix()`. Adding a family is two edits.
|
|
155
|
+
- **Every gradient end clears 4.5:1 against its own label, and none of them clears it by much.** Both halves are the constraint: the floor stops a label from being unreadable, and holding a family further above the floor than it has to be is what makes a set of buttons quietly a shade too deep. Every stop lands within 0.15 of exactly 4.5. `warning` is the one family with dark ink, because white on amber does not reach 4.5:1 at any lightness worth calling amber.
|
|
156
|
+
- **Tinted shadows, and they are deliberately not part of the elevation ladder.** `--plass-{color}-tint` is the difference between a button that is blue and a button that is _made of_ blue. `elevation` says how far off the page a surface is; the tint says what it is made of, and a `danger` button one level higher is not a redder pane of glass.
|
|
157
|
+
- **Press is light, not paint.** A gradient cannot be transitioned, so hover and press are `filter: brightness()` — 1.05 up, 0.95 down — with the elevation and the tint moving in the same direction. One duration and one curve, applied identically in both directions.
|
|
158
|
+
- **`.plass-glow`**, the interaction light, as real CSS rather than a Tailwind arbitrary variant: a bloom that follows the pointer across a control and a brighter flash on press that lands in `0ms` and drains over ~700ms. The component writes `--p-mx`/`--p-my` straight to the element's inline style on `pointermove`, so React never re-renders and nothing reflows. On a touch screen — where there is no hover — the press layer is what carries it, following a finger dragged across the button.
|
|
159
|
+
- `src/types.ts` — `PlassSize`, `PlassColor`, `PlassDensity`, `PlassVariant`, `PlassElevation`, `PlassOrientation`, `PlassSide`, `PlassAlign` and the `PlassStyleProps` bundle. A `size` of `md` is 40px on everything, and an idea that already has a name does not get a second one.
|
|
160
|
+
|
|
161
|
+
### Documentation
|
|
162
|
+
|
|
163
|
+
- The VitePress site, in English and Korean, with every preview a real React island rendering the components from `src/` rather than a screenshot: a home page with a live sign-in hero and a full sample screen, the component index, a page each for Button and TextField, and three design pages — design language, colour and prop conventions.
|
|
164
|
+
- `docs/public/llms.txt`, the whole site flattened for an agent.
|
|
165
|
+
|
|
166
|
+
### Notes
|
|
167
|
+
|
|
168
|
+
- Node 20.19 or later, React 18 or 19, one runtime dependency (`@base-ui/react`).
|
|
169
|
+
- The tokens use `color-mix()` and `backdrop-filter`. Where `backdrop-filter` is missing only the blur drops out.
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<img src="https://plass.cdget.com/128x128.png" alt="Plass UI" width="96" height="96" />
|
|
2
|
+
|
|
1
3
|
# Plass UI for React
|
|
2
4
|
|
|
3
5
|
[](https://github.com/jooy2/plass-ui/blob/main/LICENSE) [](https://www.npmjs.com/package/plass-ui) [](https://www.npmjs.com/package/plass-ui)
|
|
@@ -8,22 +10,17 @@ Live previews and full props for every component — pick **React** in the sideb
|
|
|
8
10
|
|
|
9
11
|
---
|
|
10
12
|
|
|
11
|
-
**Plass
|
|
12
|
-
|
|
13
|
-
A thing that is pressed is **tinted glass**: a gradient that sweeps between two ends of its colour family at 135°, a drop shadow tinted with that family, and a bloom of light that follows the pointer across it. A thing that holds something is **clear glass**: translucent, heavily blurred, a white hairline round it, and never dyed. There is no third answer.
|
|
13
|
+
> **Plass is one design language, shipped as two libraries.** Every surface answers a single question — is this pressed, or does it hold something? A thing you press is **tinted glass**: a gradient that _turns_ through its colour family rather than darkening, a shadow thrown in that same colour, and a bloom of light that follows your pointer across it. A thing that holds something is **clear glass**: translucent, deeply blurred, edged with a white hairline, never dyed. Nothing is bevelled, nothing is embossed and nothing moves when you press it — depth is carried by light, colour and blur.
|
|
14
14
|
|
|
15
|
-
This is the React half
|
|
15
|
+
This is the React half. The [Flutter package](https://pub.dev/packages/plass_ui) is the other one, and the two hold the same components under the same names, the same prop vocabulary and the same numbers. They version independently, so this package's number and the pub.dev one's will not always agree.
|
|
16
16
|
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **Accessible
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **One runtime dependency**, [Base UI](https://base-ui.com), which is where the interaction and accessibility behaviour comes from.
|
|
25
|
-
|
|
26
|
-
> **Both packages ship the same library.** Every component listed below exists in each, under the same prop vocabulary and the same tokens. They version independently, so this package's number and the Flutter one's will not always agree.
|
|
17
|
+
- **Finished the moment it is installed.** The gradients, the shadows, the blur, the focus ring and the press response are already decided and already agree with each other. One CSS import and the first screen looks like something.
|
|
18
|
+
- **Five props, not fifty.** `size`, `color`, `variant`, `density` and `elevation` mean the same thing on every component — an `md` control is 40px, `primary` is the same family — so the tenth one costs nothing to learn after the first.
|
|
19
|
+
- **Readable because it was measured.** Every gradient stop clears 4.5:1 against its own label, the lightest corner included. A colour choice here is not a contrast bug waiting for an audit.
|
|
20
|
+
- **Accessible without the checklist.** Real roles, labels, focus management and keyboard support, not `div`s with click handlers.
|
|
21
|
+
- **Dark mode you do not write.** Follows `prefers-color-scheme`, and can be forced either way on any subtree. No second palette, no colours redeclared.
|
|
22
|
+
- **Types in the box.** TypeScript declarations ship with the package, so your editor knows the prop names and the values they take before you do.
|
|
23
|
+
- **Nothing you did not ask for.** ESM only, tree-shakeable, a single runtime dependency, and an entry point per component for a build that cannot shake a barrel.
|
|
27
24
|
|
|
28
25
|
## Install
|
|
29
26
|
|
|
@@ -105,7 +102,9 @@ Same component, same types. The barrel is the one to reach for by default; this
|
|
|
105
102
|
|
|
106
103
|
### Next.js and server components
|
|
107
104
|
|
|
108
|
-
|
|
105
|
+
Nearly every component carries `'use client'`, so a Server Component can import one directly and there is nothing to configure — no `transpilePackages`, no `next.config` entry, no provider. What the directive cannot do is carry a function across the server boundary: a file that passes `onClick`, `onValueChange` or `render` needs its own `'use client'`, which is React's rule for every client component rather than this library's. Outside a server-component graph the directive is inert.
|
|
106
|
+
|
|
107
|
+
`PlTable` is the exception and has no directive, because that rule would otherwise have cost it its own API: every column is a `render` callback, and a table belongs on a page that fetches its own rows. A Server Component renders it whole; a client module that imports it gets a client component, as it does for anything else it imports.
|
|
109
108
|
|
|
110
109
|
### Dark mode
|
|
111
110
|
|
|
@@ -67,6 +67,20 @@ export interface PlAccordionItemProps extends Omit<React.ComponentPropsWithoutRe
|
|
|
67
67
|
* other — the browser rewrites a `<button>` inside a `<button>` on parse.
|
|
68
68
|
*/
|
|
69
69
|
action?: React.ReactNode;
|
|
70
|
+
/**
|
|
71
|
+
* Holds the header's title and subtitle to one line each, ellipsing whatever
|
|
72
|
+
* runs past.
|
|
73
|
+
*
|
|
74
|
+
* **Off, and that is the reversal of what this used to do.** A fold's title is
|
|
75
|
+
* a heading rather than a cell — an accordion is most often a list of
|
|
76
|
+
* questions, and a question is a sentence. Ellipsing one costs the reader the
|
|
77
|
+
* end of it with no tooltip and no way to see it, while wrapping costs a row
|
|
78
|
+
* that is two lines tall in a component whose whole job is to change height.
|
|
79
|
+
* The one place the old behaviour is right is a header carrying a name from a
|
|
80
|
+
* database beside a control, and that is what this prop is for.
|
|
81
|
+
* @default false
|
|
82
|
+
*/
|
|
83
|
+
truncate?: boolean;
|
|
70
84
|
/** Unavailable. This section stops folding; the rest keep working. */
|
|
71
85
|
disabled?: boolean;
|
|
72
86
|
/** The body. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import*as l from"react";import{Accordion as s}from"@base-ui/react/accordion";import{ChevronIcon as a}from"../../internal/icons.js";import{focusRingClasses as n,focusRingInsetClasses as i,gapClasses as r,hasContent as d,iconClasses as o,metaTextClasses as c,radiusClasses as
|
|
1
|
+
"use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import*as l from"react";import{Accordion as s}from"@base-ui/react/accordion";import{ChevronIcon as a}from"../../internal/icons.js";import{focusRingClasses as n,focusRingInsetClasses as i,gapClasses as r,hasContent as d,iconClasses as o,metaTextClasses as c,radiusClasses as m,sheetBodyClasses as p,sheetHeaderGapClasses as f,sheetPaddingXClasses as u,sheetPaddingYClasses as x,sheetRestClasses as h,sheetTitleClasses as v,surfaceSlots as b,transitionClasses as g}from"../../internal/styles.js";const N=/* @__PURE__ */l.createContext({size:"md",density:"default",dividers:!0}),j={xs:m.xs,sm:m.xs,md:m.sm,lg:m.sm,xl:m.md},y={default:{xs:"pt-1.5",sm:"pt-2",md:"pt-3",lg:"pt-3.5",xl:"pt-4"},compact:{xs:"pt-1",sm:"pt-1.5",md:"pt-2",lg:"pt-2.5",xl:"pt-3"}},w={default:{xs:"pb-2.5",sm:"pb-3",md:"pb-5",lg:"pb-6",xl:"pb-7"},compact:{xs:"pb-2",sm:"pb-2.5",md:"pb-3.5",lg:"pb-4",xl:"pb-5"}};export const PlAccordion=/* @__PURE__ */l.forwardRef(function PlAccordion({variant:t="glass",size:a="md",color:n="primary",density:i="default",elevation:r=0,multiple:d=!1,value:o,defaultValue:c,onValueChange:p,dividers:f=!0,disabled:u=!1,hiddenUntilFound:x=!1,keepMounted:v=!1,className:j,style:y,children:w,...$},P){const k=l.useMemo(()=>({size:a,density:i,dividers:f}),[a,i,f]),A=["flex flex-col",m[a],h[t],g,f?"overflow-hidden [&>div+div]:border-t [&>div+div]:[border-color:var(--plass-divider)]":"p-1",j??""].filter(Boolean).join(" ");return e(N.Provider,{value:k,children:e(s.Root,{ref:P,multiple:d,value:o,defaultValue:c,onValueChange:e=>p?.(e),disabled:u,hiddenUntilFound:x,keepMounted:v,className:A,style:{...b(n,r),...y},...$,children:w})})});export const PlAccordionItem=/* @__PURE__ */l.forwardRef(function PlAccordionItem({value:m,title:h,subtitle:b,startIcon:$,action:P,truncate:k=!1,disabled:A=!1,className:z,children:C,...I},V){const{size:_,density:B,dividers:M}=l.useContext(N),R=u[B][_],F=x[B][_],U=k?"truncate":"";return t(s.Item,{ref:V,value:m,disabled:A,className:["flex flex-col",z??""].filter(Boolean).join(" "),...I,children:[t(s.Header,{className:"m-0 flex w-full items-center [font:inherit]",children:[t(s.Trigger,{className:["flex min-w-0 flex-1 cursor-pointer items-center text-start",R,F,r[_],g,o,M?i:`${n} ${j[_]}`,"hover:bg-(--p-soft)","data-[panel-open]:bg-(--p-soft) data-[panel-open]:text-(--p-accent)","disabled:cursor-not-allowed disabled:bg-transparent disabled:opacity-50"].filter(Boolean).join(" "),children:[d($)?e("span",{className:"flex h-[1lh] shrink-0 items-center text-(--plass-muted-fg)",children:$}):null,t("span",{className:`flex min-w-0 flex-1 flex-col ${f[_]}`,children:[d(h)?e("span",{className:`plass-title font-semibold ${U} ${v[_]}`.trim(),children:h}):null,d(b)?e("span",{className:`text-(--plass-muted-fg) ${U} ${c[_]}`.trim(),children:b}):null]}),e("span",{className:["flex h-[1lh] shrink-0 items-center text-(--plass-muted-fg)","[transition:rotate_var(--plass-duration)_var(--plass-ease)]","data-[panel-open]:rotate-180"].join(" "),children:e(a,{})})]}),d(P)?e("span",{className:`flex shrink-0 items-center ${R}`,children:P}):null]}),e(s.Panel,{className:["h-(--accordion-panel-height) overflow-hidden","[transition:height_var(--plass-duration-slow)_var(--plass-ease)]","data-[starting-style]:h-0 data-[ending-style]:h-0"].join(" "),children:e("div",{className:["text-(--plass-muted-fg)",p[_],R,y[B][_],w[B][_]].join(" "),children:C})})]})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as e,jsxs as a}from"react/jsx-runtime";import*as r from"react";import{Avatar as t}from"@base-ui/react/avatar";import{controlHeightClasses as
|
|
1
|
+
"use client";import{jsx as e,jsxs as a}from"react/jsx-runtime";import*as r from"react";import{Avatar as t}from"@base-ui/react/avatar";import{AvatarGroupContext as s}from"../../internal/avatar-group.js";import{controlHeightClasses as o,controlSlots as n,controlSquareClasses as l,glassClasses as i,hasContent as c,radiusClasses as d,srOnlyClasses as m,transitionClasses as p}from"../../internal/styles.js";const f={xs:"text-[0.5625rem]",sm:"text-[0.6875rem]",md:"text-[0.8125rem]",lg:"text-[1rem]",xl:"text-[1.1875rem]"},v={solid:/* @__PURE__ */["text-(--p-on-solid) [background-image:var(--p-fill)]","[box-shadow:var(--p-elev),var(--p-lift)]"].join(" "),glass:/* @__PURE__ */[i,"border text-(--p-accent) bg-(--plass-glass)","[border-color:var(--plass-border)]","[box-shadow:var(--p-elev),var(--plass-gloss-glass)]"].join(" "),ghost:"text-(--p-accent) bg-(--p-soft-press) [box-shadow:var(--p-elev)]"},g=/* @__PURE__ */["relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden","align-middle leading-none font-semibold tracking-wide whitespace-nowrap","[&_svg]:pointer-events-none [&_svg]:size-[55%]",p].join(" ");function PersonIcon(){return a("svg",{viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true",children:[e("path",{d:"M12 12a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9Z"}),e("path",{d:"M12 14.25c-4.28 0-7.75 2.42-7.75 5.4 0 .75.6 1.35 1.35 1.35h12.8c.75 0 1.35-.6 1.35-1.35 0-2.98-3.47-5.4-7.75-5.4Z"})]})}export const PlAvatar=/* @__PURE__ */r.forwardRef(function PlAvatar({src:i,srcSet:p,alt:h,name:u,initials:x,shape:b,variant:j,size:y,color:w,elevation:N,delay:z,imageProps:C,onLoadingStatusChange:A,className:P,style:k,children:B,...S},I){const L=r.useContext(s),F=b??L?.shape??"circle",M=j??L?.variant??"ghost",R=y??L?.size??"md",Z=w??L?.color??"primary",_=N??L?.elevation??0,O=u?function initialsOf(e){const a=e.normalize("NFC").trim().split(/\s+/).filter(Boolean);return 0===a.length?"":((Array.from(a[0])[0]??"")+(a.length>1?Array.from(a[a.length-1])[0]??"":"")).toLocaleUpperCase()}(u):"",U=h??u,q=c(B)?B:(x??O)||e(PersonIcon,{}),D=c(B)||Boolean(x??O),E=[g,o[R],l[R],f[R],"circle"===F?"rounded-full":d[R],v[M],P??""].filter(Boolean).join(" ");return a(t.Root,{ref:I,className:E,style:{...n(Z,_,M),...k},...S,children:[i?e(t.Image,{src:i,srcSet:p,alt:U??"",className:"size-full object-cover",onLoadingStatusChange:A,...C}):null,a(t.Fallback,{delay:i?z:void 0,className:"flex size-full items-center justify-center",children:[U&&D?e("span",{className:m,children:U}):null,e("span",{"aria-hidden":!(!U||!D)||void 0,className:"contents",children:q})]})]})});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type PlAvatarShape } from '../avatar/PlAvatar.js';
|
|
3
|
+
import type { PlassColor, PlassElevation, PlassSize, PlassVariant } from '../../types.js';
|
|
4
|
+
export interface PlAvatarGroupProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'color'> {
|
|
5
|
+
/**
|
|
6
|
+
* How many avatars are drawn before the rest become a count. Left out, every
|
|
7
|
+
* one of them is drawn.
|
|
8
|
+
*/
|
|
9
|
+
max?: number;
|
|
10
|
+
/**
|
|
11
|
+
* How many there are altogether, when the group was handed only the first
|
|
12
|
+
* few. Without it the count is worked out from the children, which is right
|
|
13
|
+
* only when all of them were passed.
|
|
14
|
+
*/
|
|
15
|
+
total?: number;
|
|
16
|
+
/**
|
|
17
|
+
* How far each avatar sits under the one before it — a number of pixels or
|
|
18
|
+
* any CSS length. Left out it is a fraction of `size`, which keeps the
|
|
19
|
+
* overlap looking the same at every step.
|
|
20
|
+
*/
|
|
21
|
+
overlap?: number | string;
|
|
22
|
+
/** Passed to every avatar in the group. @default 'md' */
|
|
23
|
+
size?: PlassSize;
|
|
24
|
+
/** Passed to every avatar in the group. @default 'circle' */
|
|
25
|
+
shape?: PlAvatarShape;
|
|
26
|
+
/** Passed to every avatar in the group. @default 'ghost' */
|
|
27
|
+
variant?: PlassVariant;
|
|
28
|
+
/** Passed to every avatar in the group. @default 'primary' */
|
|
29
|
+
color?: PlassColor;
|
|
30
|
+
/** Passed to every avatar in the group. @default 0 */
|
|
31
|
+
elevation?: PlassElevation;
|
|
32
|
+
/** The avatars. */
|
|
33
|
+
children?: React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A stack of avatars, overlapping, with the ones that did not fit as a count.
|
|
37
|
+
*
|
|
38
|
+
* `size`, `shape`, `variant`, `color` and `elevation` are set once here rather
|
|
39
|
+
* than on every avatar — a stack whose fourth face is a size out is not a stack
|
|
40
|
+
* — and an avatar's own prop still wins, which is what lets one of them be
|
|
41
|
+
* marked out from the rest.
|
|
42
|
+
*
|
|
43
|
+
* The order is the DOM order, and each face overlaps the one before it — so the
|
|
44
|
+
* last avatar in the list is the one in front, and the `+n` sits over all of
|
|
45
|
+
* them.
|
|
46
|
+
*/
|
|
47
|
+
export declare const PlAvatarGroup: React.ForwardRefExoticComponent<PlAvatarGroupProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as r,jsxs as a}from"react/jsx-runtime";import*as e from"react";import{PlAvatar as t}from"../avatar/PlAvatar.js";import{AvatarGroupContext as i}from"../../internal/avatar-group.js";import{cx as l,toLength as s}from"../../internal/styles.js";const o={xs:"0.5rem",sm:"0.625rem",md:"0.875rem",lg:"1rem",xl:"1.25rem"};export const PlAvatarGroup=/* @__PURE__ */e.forwardRef(function PlAvatarGroup({max:n,total:m,overlap:c,size:p="md",shape:v="circle",variant:d="ghost",color:f="primary",elevation:h=0,className:u,style:x,children:g,...j},y){const A=e.useMemo(()=>({size:p,shape:v,variant:d,color:f,elevation:h}),[p,v,d,f,h]),P=e.Children.toArray(g),M=void 0===n?P:P.slice(0,Math.max(0,n)),z=m??P.length,G=Math.max(0,z-M.length);return r(i.Provider,{value:A,children:a("div",{ref:y,className:l("isolate inline-flex items-center","[&>*:not(:first-child)]:[margin-inline-start:calc(var(--p-overlap)*-1)]","[&>*]:ring-2 [&>*]:ring-(--plass-surface)",u??""),style:{"--p-overlap":void 0===c?o[p]:s(c),...x},...j,children:[M,G>0?r(t,{initials:`+${G}`}):null]})})});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{PlAvatarGroup}from"./PlAvatarGroup.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as e,jsxs as
|
|
1
|
+
"use client";import{jsx as e,jsxs as a}from"react/jsx-runtime";import*as r from"react";import{Checkbox as t}from"@base-ui/react/checkbox";import{Field as o}from"@base-ui/react/field";import{controlSlots as s,focusRingClasses as l,glassClasses as i,hasContent as n,metaTextClasses as c,tickRadiusClasses as d,tickRowTextClasses as p,tickSizeClasses as h,transitionClasses as m}from"../../internal/styles.js";const u=/* @__PURE__ */["relative inline-flex shrink-0 items-center justify-center border","[-webkit-tap-highlight-color:transparent] [touch-action:manipulation]",m,l].join(" "),f=/* @__PURE__ */[i,"cursor-pointer bg-(--plass-glass) [border-color:var(--plass-border)]","hover:bg-(--plass-glass-hover) hover:[border-color:var(--p-line)]","data-[checked]:[background-image:var(--p-fill)] data-[checked]:text-(--p-on-solid)","data-[checked]:[border-color:transparent] data-[checked]:hover:brightness-105","data-[indeterminate]:[background-image:var(--p-fill)] data-[indeterminate]:text-(--p-on-solid)","data-[indeterminate]:[border-color:transparent]"].join(" "),k=/* @__PURE__ */[i,"cursor-default bg-(--plass-glass) [border-color:var(--plass-border)]","saturate-[0.55]","data-[checked]:[background-image:var(--p-fill)] data-[checked]:text-(--p-on-solid)","data-[checked]:[border-color:transparent]"].join(" "),b=/* @__PURE__ */[i,"cursor-not-allowed bg-(--plass-glass) [border-color:var(--plass-border)]","opacity-50 saturate-[0.35]","data-[checked]:[background-image:var(--p-fill)] data-[checked]:text-(--p-on-solid)","data-[checked]:[border-color:transparent]","data-[indeterminate]:[background-image:var(--p-fill)] data-[indeterminate]:text-(--p-on-solid)"].join(" ");function CheckMark(){return e("svg",{viewBox:"0 0 12 12",fill:"none","aria-hidden":"true",className:"size-full",children:e("path",{d:"M2 6.2 4.6 8.8 10 3.4",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round"})})}function DashMark(){return e("svg",{viewBox:"0 0 12 12",fill:"none","aria-hidden":"true",className:"size-full",children:e("path",{d:"M2.5 6h7",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round"})})}export const PlCheckbox=/* @__PURE__ */r.forwardRef(function PlCheckbox({size:r="md",color:l="primary",label:i,description:m,error:g,invalid:x,disabled:v=!1,readOnly:j=!1,className:N,style:y,...C},w){const M=n(g),z=x??M,L=z?"danger":l,$=[u,h[r],d[r],v?b:j?k:f].join(" ");return a(o.Root,{disabled:v,invalid:z,className:["inline-flex flex-col gap-1 align-top",N??""].filter(Boolean).join(" "),style:{...s(L,0,"solid"),...y},children:[a("div",{className:`flex items-start gap-2 ${p[r]}`,children:[e("span",{className:"flex h-[1lh] shrink-0 items-center",children:e(t.Root,{ref:w,className:$,disabled:v,readOnly:j,...C,children:e(t.Indicator,{className:"flex size-[70%] items-center justify-center",children:C.indeterminate?e(DashMark,{}):e(CheckMark,{})})})}),i||m?a("span",{className:"flex min-w-0 flex-col gap-0.5",children:[i?e(o.Label,{className:v?"text-(--plass-muted-fg)":"cursor-pointer text-(--plass-fg)",children:i}):null,m?e(o.Description,{className:`${c[r]} text-(--plass-muted-fg)`,children:m}):null]}):null]}),e(o.Error,M?{match:!0,className:`${c[r]} text-(--p-accent)`,children:g}:{className:`${c[r]} text-(--p-accent)`})]})});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { PlColorFormat } from '../../internal/color.js';
|
|
3
|
+
import type { PlassElevation, PlassStyleProps } from '../../types.js';
|
|
4
|
+
export type { PlColorFormat } from '../../internal/color.js';
|
|
5
|
+
/** The names for the parts of the picker that have no text on them. */
|
|
6
|
+
export interface PlColorPickerLabels {
|
|
7
|
+
/** The saturation/brightness square. */
|
|
8
|
+
area: string;
|
|
9
|
+
/** The hue rail beside it. */
|
|
10
|
+
hue: string;
|
|
11
|
+
/** The opacity rail, when `alpha` is on. */
|
|
12
|
+
alpha: string;
|
|
13
|
+
/** The field the value can be typed into. */
|
|
14
|
+
value: string;
|
|
15
|
+
/** The grid of ready-made colours. */
|
|
16
|
+
swatches: string;
|
|
17
|
+
/** The × that empties the control. */
|
|
18
|
+
clear: string;
|
|
19
|
+
/** What the trigger reads before anything has been chosen. */
|
|
20
|
+
empty: string;
|
|
21
|
+
}
|
|
22
|
+
/** The English the picker uses for the parts nobody can read a name off. */
|
|
23
|
+
export declare const defaultColorPickerLabels: PlColorPickerLabels;
|
|
24
|
+
export interface PlColorPickerProps extends PlassStyleProps, Omit<React.ComponentPropsWithoutRef<'div'>, 'color' | 'defaultValue' | 'onChange'> {
|
|
25
|
+
/** The colour, as a CSS string. Pass it to drive the picker yourself. */
|
|
26
|
+
value?: string;
|
|
27
|
+
/** Where an uncontrolled picker starts. @default '#1a58d1' */
|
|
28
|
+
defaultValue?: string;
|
|
29
|
+
/** Called with the new colour, written in `format`. */
|
|
30
|
+
onValueChange?: (value: string) => void;
|
|
31
|
+
/** Which notation the value is written in on the way out. @default 'hex' */
|
|
32
|
+
format?: PlColorFormat;
|
|
33
|
+
/** Offers an opacity rail, and lets the value carry a fourth channel. @default false */
|
|
34
|
+
alpha?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* The ready-made colours under the panel. `false` draws none; an array of CSS
|
|
37
|
+
* colour strings replaces the built-in set.
|
|
38
|
+
*/
|
|
39
|
+
swatches?: readonly string[] | false;
|
|
40
|
+
/** Draws the panel in the page instead of in a popup, with no trigger. @default false */
|
|
41
|
+
inline?: boolean;
|
|
42
|
+
/** The field under the panel that the value can be typed into. @default true */
|
|
43
|
+
editable?: boolean;
|
|
44
|
+
/** Label above the control. */
|
|
45
|
+
label?: React.ReactNode;
|
|
46
|
+
/** Helper text below it. */
|
|
47
|
+
description?: React.ReactNode;
|
|
48
|
+
/** Error message below. Its presence also turns the control invalid. */
|
|
49
|
+
error?: React.ReactNode;
|
|
50
|
+
invalid?: boolean;
|
|
51
|
+
required?: boolean;
|
|
52
|
+
disabled?: boolean;
|
|
53
|
+
/** Shows the colour and forbids changing it. @default false */
|
|
54
|
+
readOnly?: boolean;
|
|
55
|
+
/** Stretches the trigger to its container. @default false */
|
|
56
|
+
fullWidth?: boolean;
|
|
57
|
+
/** Offers the × that empties the control. @default false */
|
|
58
|
+
clearable?: boolean;
|
|
59
|
+
/** Submits with a form under this name. */
|
|
60
|
+
name?: string;
|
|
61
|
+
/** Whether the popup is open. Pass it to drive the popup yourself. */
|
|
62
|
+
open?: boolean;
|
|
63
|
+
/** Where an uncontrolled popup starts. @default false */
|
|
64
|
+
defaultOpen?: boolean;
|
|
65
|
+
onOpenChange?: (open: boolean) => void;
|
|
66
|
+
/** Overrides for the accessible names, one at a time. */
|
|
67
|
+
labels?: Partial<PlColorPickerLabels>;
|
|
68
|
+
/** Drop shadow depth on the trigger. `0` — the default — is flat. @default 0 */
|
|
69
|
+
elevation?: PlassElevation;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A colour, chosen by eye.
|
|
73
|
+
*
|
|
74
|
+
* A saturation square with a hue rail beside it — the arrangement every design
|
|
75
|
+
* tool has settled on, because it is the one that puts every colour of a hue
|
|
76
|
+
* within a single movement of the pointer. `alpha` adds a third rail, `format`
|
|
77
|
+
* decides which notation comes back out, and `swatches` puts the handful of
|
|
78
|
+
* colours a product actually uses one click away.
|
|
79
|
+
*
|
|
80
|
+
* The panel's state is **HSV and it never leaves that model**, which is what
|
|
81
|
+
* keeps the hue rail still while the pointer is in the black corner: through
|
|
82
|
+
* RGB, every shade of black is the same colour and the rail would snap to red.
|
|
83
|
+
*
|
|
84
|
+
* There is no colour library under this. The conversions are in
|
|
85
|
+
* `internal/color.ts`, which is a hundred lines of arithmetic — the whole reason
|
|
86
|
+
* no colour library comes with it.
|
|
87
|
+
*/
|
|
88
|
+
export declare const PlColorPicker: React.ForwardRefExoticComponent<PlColorPickerProps & React.RefAttributes<HTMLDivElement>>;
|