@lotics/ui 23.2.0 → 24.0.2
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/AGENTS.md +8 -1
- package/MIGRATION.md +40 -3
- package/docs/catalog.md +62 -13
- package/docs/composition.md +63 -3
- package/docs/data_entry.md +19 -1
- package/docs/templates.md +24 -28
- package/examples/tpl_item_list.tsx +19 -768
- package/examples/tpl_record.tsx +515 -415
- package/examples/tpl_task_board.tsx +2 -2
- package/package.json +5 -2
- package/src/deadline.ts +157 -0
- package/src/filter_chip.tsx +2 -2
- package/src/locale.tsx +23 -0
- package/src/option_list.tsx +3 -3
- package/src/reference_field.tsx +178 -0
- package/src/table.tsx +9 -0
- package/src/text_button.tsx +139 -0
- package/src/text_link.tsx +24 -14
- package/src/use_section_nav.ts +31 -1
package/src/text_link.tsx
CHANGED
|
@@ -1,39 +1,49 @@
|
|
|
1
1
|
import { Text, type TextProps } from "./text";
|
|
2
2
|
import { Icon, type IconName } from "./icon";
|
|
3
|
+
import { colors } from "./colors";
|
|
3
4
|
import { getTextColor } from "./text_utils";
|
|
4
5
|
|
|
5
6
|
export interface TextLinkProps extends TextProps {
|
|
6
7
|
/** Optional leading icon. */
|
|
7
8
|
icon?: IconName;
|
|
8
9
|
/** A URL — renders a real anchor (`<a href>` on web; middle-click / open-in-new-tab
|
|
9
|
-
* work). For in-app routing, wrap `TextLink`
|
|
10
|
+
* work). For in-app routing, wrap `TextLink` in your own pressable. */
|
|
10
11
|
href?: string;
|
|
11
|
-
/** An in-app action. With NEITHER `href` nor `onPress`, `TextLink` is just
|
|
12
|
-
* underlined text you wrap in your own pressable (e.g. a table cell). */
|
|
13
|
-
onPress?: () => void;
|
|
14
12
|
}
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
|
-
* Underlined text that
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* inherits every `Text` prop) — the neutral,
|
|
21
|
-
* which is fixed blue + `role="link"` for the
|
|
15
|
+
* Underlined text that NAVIGATES — `href` renders a real anchor; with no `href`
|
|
16
|
+
* it is plain underlined text you drop inside your own pressable (a table cell, a
|
|
17
|
+
* custom row), or a MARKER that a value leads somewhere (a record reference).
|
|
18
|
+
* Colour comes from `color` (it inherits every `Text` prop) — the neutral,
|
|
19
|
+
* configurable counterpart to `Link`, which is fixed blue + `role="link"` for the
|
|
20
|
+
* "this opens somewhere else" signal.
|
|
21
|
+
*
|
|
22
|
+
* It does NOT act. An `onPress` here used to resolve to `role="button"`, so one
|
|
23
|
+
* export was three components — a link, a button, and a passive marker — behind
|
|
24
|
+
* one name, inside `OptionList`, `FilterChip` and others. That mode is
|
|
25
|
+
* `TextButton` now — also underlined, since that is what marks a surface-less
|
|
26
|
+
* control interactive, but in ZINC: blue is reserved for navigation, so the ink is
|
|
27
|
+
* what says whether a press moves you or acts. Pick by what the press DOES.
|
|
22
28
|
*/
|
|
23
29
|
export function TextLink(props: TextLinkProps) {
|
|
24
|
-
const { icon, href,
|
|
25
|
-
const interactive = href != null || onPress != null;
|
|
30
|
+
const { icon, href, children, color, style, ...textProps } = props;
|
|
26
31
|
return (
|
|
27
32
|
<Text
|
|
28
33
|
decoration="underline"
|
|
29
34
|
weight="medium"
|
|
30
35
|
color={color}
|
|
31
36
|
href={href}
|
|
32
|
-
|
|
33
|
-
accessibilityRole={href != null ? "link" : onPress != null ? "button" : undefined}
|
|
37
|
+
accessibilityRole={href != null ? "link" : undefined}
|
|
34
38
|
style={[
|
|
35
39
|
icon ? { display: "flex", flexDirection: "row", alignItems: "center", gap: 4 } : null,
|
|
36
|
-
|
|
40
|
+
// Blue only when it actually NAVIGATES. An `href` is a destination, so it
|
|
41
|
+
// takes the navigation ink (`color` still overrides). Without one this is
|
|
42
|
+
// underlined text you wrap yourself, or a MARKER on a value that leads
|
|
43
|
+
// somewhere — the record's customer field is one, and pressing it opens a
|
|
44
|
+
// PEEK, so blue there would promise a trip it never takes.
|
|
45
|
+
href != null && color == null ? { color: colors.blue[600] } : null,
|
|
46
|
+
href != null ? { cursor: "pointer" } : null,
|
|
37
47
|
style,
|
|
38
48
|
]}
|
|
39
49
|
{...textProps}
|
package/src/use_section_nav.ts
CHANGED
|
@@ -14,6 +14,14 @@ export interface SectionNavHandle<K extends string> {
|
|
|
14
14
|
onScroll: (e: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* How close to the content's end still counts as "at the end". Fractional
|
|
19
|
+
* scroll offsets (zoom, hi-dpi, momentum) land a pixel or two short of the
|
|
20
|
+
* exact bottom, and a strict equality would drop the rule on the frame that
|
|
21
|
+
* matters most — the one the reader stops on.
|
|
22
|
+
*/
|
|
23
|
+
const END_SLACK = 2;
|
|
24
|
+
|
|
17
25
|
/**
|
|
18
26
|
* Scroll-spy for a LONG record surface with a left outline rail: one scrolling
|
|
19
27
|
* page whose sections register their offsets, a rail of `MenuButton`s that
|
|
@@ -39,6 +47,10 @@ export interface SectionNavHandle<K extends string> {
|
|
|
39
47
|
* grid, a late-loading table) can never stale either. On native there is no
|
|
40
48
|
* re-measure — content above should settle before precision matters
|
|
41
49
|
* (`onLayout` won't refire on a pure position shift).
|
|
50
|
+
*
|
|
51
|
+
* The LAST key is active whenever the scroll reaches the end, regardless of
|
|
52
|
+
* offsets: a final section shorter than the viewport never scrolls its top past
|
|
53
|
+
* the trigger line, so without that rule the rail can never point at it.
|
|
42
54
|
*/
|
|
43
55
|
export function useSectionNav<K extends string>(keys: readonly [K, ...K[]]): SectionNavHandle<K> {
|
|
44
56
|
const scrollRef = useRef<ScrollView>(null);
|
|
@@ -93,13 +105,31 @@ export function useSectionNav<K extends string>(keys: readonly [K, ...K[]]): Sec
|
|
|
93
105
|
scrollRef.current?.scrollTo({ y: Math.max(0, y - 12), animated: true });
|
|
94
106
|
};
|
|
95
107
|
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
96
|
-
const
|
|
108
|
+
const { contentOffset, layoutMeasurement, contentSize } = e.nativeEvent;
|
|
109
|
+
const y = contentOffset.y + 80;
|
|
97
110
|
const offsets = measure();
|
|
98
111
|
let cur: K = keys[0];
|
|
99
112
|
for (const k of keys) {
|
|
100
113
|
const sy = offsets[k];
|
|
101
114
|
if (sy != null && sy <= y) cur = k;
|
|
102
115
|
}
|
|
116
|
+
// THE LAST SECTION IS OTHERWISE UNREACHABLE. A section opening within one
|
|
117
|
+
// viewport of the content's end can never scroll its top past the trigger
|
|
118
|
+
// line: at maximum scroll `y` is `contentHeight - viewportHeight + 80`,
|
|
119
|
+
// which is still ABOVE it, so the rule above pins the rail to the section
|
|
120
|
+
// before it and the reader watches the wrong item stay lit while they read.
|
|
121
|
+
// A trailing spacer would "fix" it by making every record end in blank
|
|
122
|
+
// space, which is paying in layout for an arithmetic problem.
|
|
123
|
+
//
|
|
124
|
+
// At the end of the scroll there is nothing left to disambiguate — the
|
|
125
|
+
// reader is as deep into the last section as the page permits — so the end
|
|
126
|
+
// of the scroll IS the last section. Guarded on both measurements being
|
|
127
|
+
// present: RN gives them on every scroll event, but a caller synthesising
|
|
128
|
+
// one cannot be assumed to, and without them "at the end" is unknowable.
|
|
129
|
+
if (layoutMeasurement != null && contentSize != null) {
|
|
130
|
+
const remaining = contentSize.height - (contentOffset.y + layoutMeasurement.height);
|
|
131
|
+
if (remaining <= END_SLACK) cur = keys[keys.length - 1];
|
|
132
|
+
}
|
|
103
133
|
if (cur !== activeKey) setActiveKey(cur);
|
|
104
134
|
};
|
|
105
135
|
return { scrollRef, activeKey, register, jumpTo, onScroll };
|