@botiverse/raft-ui 0.90.0 → 0.92.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 +17 -0
- package/dist/components/fit-toolbar/fit-toolbar.d.mts +1 -1
- package/dist/components/fit-toolbar/fit-toolbar.mjs +126 -5
- package/dist/components/fit-toolbar/fit-toolbar.recipe.mjs +1 -1
- package/dist/index-exports.d.mts +236 -0
- package/dist/index-exports.mjs +142 -0
- package/dist/index.d.mts +1 -86
- package/dist/index.mjs +1 -142
- package/dist/wip-exports.d.mts +84 -0
- package/dist/wip-exports.mjs +54 -0
- package/dist/wip.d.mts +1 -28
- package/dist/wip.mjs +1 -54
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
## [0.92.0](https://github.com/botiverse/raft-design/compare/v0.91.0...v0.92.0) (2026-11-11)
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **Components used only on an app's lazy pages stay out of the chunk that loads raft-ui first.** `dist/index.mjs` and `dist/wip.mjs` are now the drag Escape guard import plus `export * from "./index-exports.mjs"` / `"./wip-exports.mjs"`; the names live in those two modules, which are not listed in `sideEffects`. Exports and types are unchanged, and the guard still installs when raft-ui loads.
|
|
6
|
+
- Why: both entries are in `sideEffects` so a bundler keeps the guard import, but a module with side effects keeps every import it has, so Rolldown put every raft-ui component an app used anywhere (the task board, InboxItem, SelectionToolbar, Meter, Search…) into that first chunk. Raft Desktop's first screen grew past its size gate (Tenny, 2026-11-11, #proj-raft-ui:49dd2bf4); with this and slock's startup closure reading the barrel, it is about 10.7 KiB gzip smaller.
|
|
7
|
+
- Affects: nothing in an app's code. A bundler that follows `sideEffects` can now leave unused components out of the first chunk.
|
|
8
|
+
|
|
9
|
+
## [0.91.0](https://github.com/botiverse/raft-design/compare/v0.90.0...v0.91.0) (2026-11-10)
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`FitToolbarSearch` cuts a placeholder that doesn't fit at a word: "Search…", not "Search …"** ([FitToolbar › Behavior](https://raft-design.botiverse.build/components/fit-toolbar/#behavior)). It measures the placeholder in the input's own font against the field's room, again when the field changes size, a font arrives or the placeholder changes, and shows the longest prefix that ends at a word (between any two characters in Chinese or Japanese) and fits with "…" after it, trailing spaces trimmed. A placeholder that fits is shown whole. `placeholder:text-ellipsis` is gone from the input; typed text still ends in an ellipsis.
|
|
14
|
+
- Why: CSS `text-overflow: ellipsis` cuts after the space and showed "Search …", which reads as a typo (Jasper, 2026-11-10, #proj-raft-ui:f9756e85).
|
|
15
|
+
- Affects: a FitToolbarSearch whose placeholder is cut; its input's `placeholder` attribute is now the cut text. The accessible name is still the whole `aria-label`. Pages pass the whole placeholder as before.
|
|
16
|
+
- Docs: https://raft-design.botiverse.build/components/fit-toolbar/
|
|
17
|
+
|
|
1
18
|
## [0.90.0](https://github.com/botiverse/raft-design/compare/v0.89.0...v0.90.0) (2026-11-10)
|
|
2
19
|
|
|
3
20
|
### Changed
|
|
@@ -292,5 +292,5 @@ export type FitToolbarSearchProps = Omit<InputGroupInputProps, "className" | "va
|
|
|
292
292
|
* field closes once it is empty and loses focus, or on Esc. A search with
|
|
293
293
|
* text stays open. Put it straight in the FitToolbar, between its groups.
|
|
294
294
|
*/
|
|
295
|
-
export declare function FitToolbarSearch({ "aria-label": label, value, defaultValue, onValueChange, minWidth, collapseOrder, icon, clearLabel, className, style, onKeyDown, disabled, ref, ...props }: FitToolbarSearchProps): import("react").JSX.Element;
|
|
295
|
+
export declare function FitToolbarSearch({ "aria-label": label, value, defaultValue, onValueChange, minWidth, collapseOrder, icon, clearLabel, className, style, onKeyDown, disabled, placeholder: hint, ref, ...props }: FitToolbarSearchProps): import("react").JSX.Element;
|
|
296
296
|
//#endregion
|
|
@@ -885,6 +885,74 @@ function coverScope(root, search) {
|
|
|
885
885
|
* isn't capped.
|
|
886
886
|
*/
|
|
887
887
|
const FIT_SEARCH_MAX_LEAST = 120;
|
|
888
|
+
const ELLIPSIS = "…";
|
|
889
|
+
/** Scripts written without spaces between words, which may be cut between any two characters. */
|
|
890
|
+
const NO_SPACES = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u;
|
|
891
|
+
/**
|
|
892
|
+
* Where a placeholder may be cut, as indices into it, shortest first: after
|
|
893
|
+
* a word followed by a space or punctuation (Intl.Segmenter's words, or the
|
|
894
|
+
* spaces without it), and between any two characters of a script written
|
|
895
|
+
* without spaces (Chinese, Japanese).
|
|
896
|
+
*/
|
|
897
|
+
function placeholderCuts(text) {
|
|
898
|
+
const words = /* @__PURE__ */ new Set();
|
|
899
|
+
const chars = [];
|
|
900
|
+
const Segmenter = typeof Intl === "undefined" ? void 0 : Intl.Segmenter;
|
|
901
|
+
if (Segmenter) {
|
|
902
|
+
const parts = Array.from(new Segmenter(void 0, { granularity: "word" }).segment(text));
|
|
903
|
+
parts.forEach((part, i) => {
|
|
904
|
+
const next = parts[i + 1];
|
|
905
|
+
if (part.isWordLike && next && !next.isWordLike) words.add(next.index);
|
|
906
|
+
});
|
|
907
|
+
} else for (const space of text.matchAll(/\S\s/g)) words.add(space.index + 1);
|
|
908
|
+
const graphemes = Segmenter ? Array.from(new Segmenter(void 0, { granularity: "grapheme" }).segment(text), (g) => ({
|
|
909
|
+
index: g.index,
|
|
910
|
+
text: g.segment
|
|
911
|
+
})) : Array.from(text).reduce((all, ch) => {
|
|
912
|
+
const last = all.at(-1);
|
|
913
|
+
all.push({
|
|
914
|
+
index: last ? last.index + last.text.length : 0,
|
|
915
|
+
text: ch
|
|
916
|
+
});
|
|
917
|
+
return all;
|
|
918
|
+
}, []);
|
|
919
|
+
graphemes.forEach((g, i) => {
|
|
920
|
+
if (i === 0) return;
|
|
921
|
+
const before = graphemes[i - 1].text;
|
|
922
|
+
if (NO_SPACES.test(before) && NO_SPACES.test(g.text)) words.add(g.index);
|
|
923
|
+
chars.push(g.index);
|
|
924
|
+
});
|
|
925
|
+
return {
|
|
926
|
+
words: [...words].sort((a, b) => a - b),
|
|
927
|
+
chars
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* The placeholder the field shows in `room` px: the whole one when it fits,
|
|
932
|
+
* else its longest prefix that ends at a cut and, trailing spaces trimmed,
|
|
933
|
+
* fits with "…" after it ("Search…", never "Search …"). A cut between
|
|
934
|
+
* characters is only for a first word too long for the room. `width` reads a
|
|
935
|
+
* text's width in the field's font.
|
|
936
|
+
*/
|
|
937
|
+
function fitPlaceholder(text, room, width) {
|
|
938
|
+
if (width(text) <= room) return text;
|
|
939
|
+
const { words, chars } = placeholderCuts(text);
|
|
940
|
+
const longest = (cuts) => {
|
|
941
|
+
let found = null;
|
|
942
|
+
let low = 0;
|
|
943
|
+
let high = cuts.length - 1;
|
|
944
|
+
while (low <= high) {
|
|
945
|
+
const mid = low + high >> 1;
|
|
946
|
+
const shown = text.slice(0, cuts[mid]).trimEnd();
|
|
947
|
+
if (shown !== "" && width(shown + ELLIPSIS) <= room) {
|
|
948
|
+
found = shown + ELLIPSIS;
|
|
949
|
+
low = mid + 1;
|
|
950
|
+
} else high = mid - 1;
|
|
951
|
+
}
|
|
952
|
+
return found;
|
|
953
|
+
};
|
|
954
|
+
return longest(words) ?? longest(chars) ?? (width(ELLIPSIS) <= room ? ELLIPSIS : "");
|
|
955
|
+
}
|
|
888
956
|
/**
|
|
889
957
|
* The search field of a list's toolbar: it fills the rest of its line and
|
|
890
958
|
* keeps at least the room its icon and its whole placeholder take, up to
|
|
@@ -895,7 +963,7 @@ const FIT_SEARCH_MAX_LEAST = 120;
|
|
|
895
963
|
* field closes once it is empty and loses focus, or on Esc. A search with
|
|
896
964
|
* text stays open. Put it straight in the FitToolbar, between its groups.
|
|
897
965
|
*/
|
|
898
|
-
function FitToolbarSearch({ "aria-label": label, value, defaultValue = "", onValueChange, minWidth, collapseOrder, icon = /* @__PURE__ */ jsx(Search, {}), clearLabel, className, style, onKeyDown, disabled, ref, ...props }) {
|
|
966
|
+
function FitToolbarSearch({ "aria-label": label, value, defaultValue = "", onValueChange, minWidth, collapseOrder, icon = /* @__PURE__ */ jsx(Search, {}), clearLabel, className, style, onKeyDown, disabled, placeholder: hint, ref, ...props }) {
|
|
899
967
|
const { search, searchButton, searchSpacer, searchField, searchInput, searchSizer } = fitToolbar();
|
|
900
968
|
const item = useFitItem({ collapse: collapseOrder });
|
|
901
969
|
const collapsed = item.taken("collapse");
|
|
@@ -911,6 +979,7 @@ function FitToolbarSearch({ "aria-label": label, value, defaultValue = "", onVal
|
|
|
911
979
|
const [setInput] = useMergedRef(input, ref);
|
|
912
980
|
const sizer = useRef(null);
|
|
913
981
|
const sizerText = useRef(null);
|
|
982
|
+
const probe = useRef(null);
|
|
914
983
|
const focusOnOpen = useRef(false);
|
|
915
984
|
const focusButtonOnClose = useRef(false);
|
|
916
985
|
useLayoutEffect(() => {
|
|
@@ -929,7 +998,7 @@ function FitToolbarSearch({ "aria-label": label, value, defaultValue = "", onVal
|
|
|
929
998
|
};
|
|
930
999
|
}, []);
|
|
931
1000
|
const [measured, setMeasured] = useState(null);
|
|
932
|
-
const placeholder = typeof
|
|
1001
|
+
const placeholder = typeof hint === "string" ? hint : label;
|
|
933
1002
|
useLayoutEffect(() => {
|
|
934
1003
|
const copy = sizer.current;
|
|
935
1004
|
const words = sizerText.current;
|
|
@@ -961,6 +1030,54 @@ function FitToolbarSearch({ "aria-label": label, value, defaultValue = "", onVal
|
|
|
961
1030
|
icon
|
|
962
1031
|
]);
|
|
963
1032
|
const least = minWidth ?? measured;
|
|
1033
|
+
const [shownHint, setShownHint] = useState(void 0);
|
|
1034
|
+
useLayoutEffect(() => {
|
|
1035
|
+
const real = input.current;
|
|
1036
|
+
const ruler = probe.current;
|
|
1037
|
+
if (typeof hint !== "string" || !real || !ruler) {
|
|
1038
|
+
setShownHint(void 0);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const widths = /* @__PURE__ */ new Map();
|
|
1042
|
+
let frame = 0;
|
|
1043
|
+
const fit = () => {
|
|
1044
|
+
frame = 0;
|
|
1045
|
+
const style = getComputedStyle(real);
|
|
1046
|
+
const room = real.getBoundingClientRect().width - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight) - parseFloat(style.borderLeftWidth) - parseFloat(style.borderRightWidth);
|
|
1047
|
+
if (room <= 0) return;
|
|
1048
|
+
const font = `${style.font}|${style.letterSpacing}`;
|
|
1049
|
+
ruler.style.font = style.font;
|
|
1050
|
+
ruler.style.letterSpacing = style.letterSpacing;
|
|
1051
|
+
const width = (text) => {
|
|
1052
|
+
const key = `${font}\n${text}`;
|
|
1053
|
+
let w = widths.get(key);
|
|
1054
|
+
if (w === void 0) {
|
|
1055
|
+
ruler.textContent = text;
|
|
1056
|
+
w = ruler.getBoundingClientRect().width;
|
|
1057
|
+
widths.set(key, w);
|
|
1058
|
+
}
|
|
1059
|
+
return w;
|
|
1060
|
+
};
|
|
1061
|
+
setShownHint(fitPlaceholder(hint, room, width));
|
|
1062
|
+
};
|
|
1063
|
+
const later = () => {
|
|
1064
|
+
if (frame === 0) frame = requestAnimationFrame(fit);
|
|
1065
|
+
};
|
|
1066
|
+
const refit = () => {
|
|
1067
|
+
widths.clear();
|
|
1068
|
+
later();
|
|
1069
|
+
};
|
|
1070
|
+
fit();
|
|
1071
|
+
const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(later);
|
|
1072
|
+
resize?.observe(real);
|
|
1073
|
+
const fonts = typeof document === "undefined" ? void 0 : document.fonts;
|
|
1074
|
+
fonts?.addEventListener?.("loadingdone", refit);
|
|
1075
|
+
return () => {
|
|
1076
|
+
if (frame !== 0) cancelAnimationFrame(frame);
|
|
1077
|
+
resize?.disconnect();
|
|
1078
|
+
fonts?.removeEventListener?.("loadingdone", refit);
|
|
1079
|
+
};
|
|
1080
|
+
}, [hint]);
|
|
964
1081
|
const change = useCallback((next) => {
|
|
965
1082
|
setOwn(next);
|
|
966
1083
|
onValueChange?.(next);
|
|
@@ -1048,12 +1165,12 @@ function FitToolbarSearch({ "aria-label": label, value, defaultValue = "", onVal
|
|
|
1048
1165
|
style: least === null ? void 0 : { "--fit-search-min": `${least}px` },
|
|
1049
1166
|
...item.props,
|
|
1050
1167
|
children: [
|
|
1051
|
-
/* @__PURE__ */
|
|
1168
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1052
1169
|
"aria-hidden": true,
|
|
1053
1170
|
inert: true,
|
|
1054
1171
|
"data-slot": "fit-toolbar-search-sizer",
|
|
1055
1172
|
className: searchSizer(),
|
|
1056
|
-
children: /* @__PURE__ */ jsxs(InputGroup, {
|
|
1173
|
+
children: [/* @__PURE__ */ jsxs(InputGroup, {
|
|
1057
1174
|
ref: sizer,
|
|
1058
1175
|
size: "md",
|
|
1059
1176
|
className: "w-max",
|
|
@@ -1062,7 +1179,10 @@ function FitToolbarSearch({ "aria-label": label, value, defaultValue = "", onVal
|
|
|
1062
1179
|
className: "whitespace-pre",
|
|
1063
1180
|
children: placeholder
|
|
1064
1181
|
})]
|
|
1065
|
-
})
|
|
1182
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1183
|
+
ref: probe,
|
|
1184
|
+
className: "absolute whitespace-pre"
|
|
1185
|
+
})]
|
|
1066
1186
|
}),
|
|
1067
1187
|
/* @__PURE__ */ jsxs(Tooltip, {
|
|
1068
1188
|
disabled: !collapsed || open,
|
|
@@ -1108,6 +1228,7 @@ function FitToolbarSearch({ "aria-label": label, value, defaultValue = "", onVal
|
|
|
1108
1228
|
className: searchInput(),
|
|
1109
1229
|
disabled,
|
|
1110
1230
|
...props,
|
|
1231
|
+
placeholder: shownHint ?? hint,
|
|
1111
1232
|
value: text,
|
|
1112
1233
|
onValueChange: change,
|
|
1113
1234
|
onKeyDown: handleKeyDown
|
|
@@ -44,7 +44,7 @@ const fitToolbar = tv({
|
|
|
44
44
|
"in-data-fit-collapse:data-open:absolute in-data-fit-collapse:data-open:min-w-0",
|
|
45
45
|
"in-data-fit-collapse:focus-within:absolute in-data-fit-collapse:focus-within:min-w-0"
|
|
46
46
|
],
|
|
47
|
-
searchInput: ["[&::-webkit-search-cancel-button]:appearance-none", "text-ellipsis
|
|
47
|
+
searchInput: ["[&::-webkit-search-cancel-button]:appearance-none", "text-ellipsis"]
|
|
48
48
|
},
|
|
49
49
|
variants: {
|
|
50
50
|
count: { true: { pickerValue: ["font-mono text-mono-meta"] } },
|