pythonnative 0.15.0__py3-none-any.whl → 0.16.0__py3-none-any.whl

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.
@@ -1,43 +1,34 @@
1
- """Built-in element factories for declarative UI composition.
1
+ """Built-in element factories and the typed prop schemas they share.
2
2
 
3
- Each function in this module returns an [`Element`][pythonnative.Element]
4
- describing a native UI widget. Element factories are pure data: no
5
- native views are created until the reconciler mounts the element tree.
3
+ Each ``@dataclass(frozen=True)`` class in this module ``TextProps``,
4
+ ``ButtonProps``, etc. is the canonical schema for one built-in
5
+ component. Each factory function (``Text``, ``Button``, …) is a thin
6
+ ergonomic wrapper that builds an [`Element`][pythonnative.Element]
7
+ through the shared :func:`_make_element` helper, so style resolution,
8
+ ``ref`` attachment, ``None``-default dropping, and forced overrides
9
+ (e.g. ``Column``'s fixed ``flex_direction``) live in exactly one place.
6
10
 
7
- All visual and layout properties are passed via the `style` parameter,
8
- which accepts a dict or a list of dicts (later entries override
9
- earlier ones; see [`resolve_style`][pythonnative.style.resolve_style]).
10
-
11
- Layout properties supported by every component:
12
-
13
- - `width`, `height`, `flex`, `flex_grow`, `flex_shrink`, `margin`,
14
- `min_width`, `max_width`, `min_height`, `max_height`, `align_self`.
15
-
16
- Flex container properties (`View` / `Column` / `Row`):
17
-
18
- - `flex_direction`, `justify_content`, `align_items`, `overflow`,
19
- `spacing`, `padding`.
20
-
21
- [`View`][pythonnative.View] is the universal flex container (like React
22
- Native's `View`). It defaults to `flex_direction: "column"`.
23
- [`Column`][pythonnative.Column] and [`Row`][pythonnative.Row] are
24
- convenience wrappers that fix the direction.
11
+ The same Props dataclasses are used by the `pythonnative.sdk` surface
12
+ for third-party components, so the built-in API and the extension API
13
+ speak the same shape.
25
14
 
26
15
  Example:
27
16
  ```python
28
17
  import pythonnative as pn
29
18
 
30
19
  pn.Column(
31
- pn.Text("Hello", style={"font_size": 18}),
20
+ pn.Text("Hello", style=pn.style(font_size=18)),
32
21
  pn.Button("Tap", on_click=lambda: print("tapped")),
33
- style={"spacing": 12, "padding": 16},
22
+ style=pn.style(spacing=12, padding=16),
34
23
  )
35
24
  ```
36
25
  """
37
26
 
27
+ from dataclasses import dataclass, field
38
28
  from typing import Any, Callable, Dict, List, Literal, Optional
39
29
 
40
30
  from .element import Element
31
+ from .sdk import Props
41
32
  from .style import (
42
33
  AutoCapitalize,
43
34
  Color,
@@ -49,33 +40,265 @@ from .style import (
49
40
  )
50
41
 
51
42
  # ======================================================================
52
- # Leaf components
43
+ # Canonical element builder
53
44
  # ======================================================================
54
45
 
55
46
 
56
- def _accessibility_props(
57
- accessibility_label: Optional[str],
58
- accessibility_hint: Optional[str],
59
- accessibility_role: Optional[str],
60
- accessible: Optional[bool],
61
- ) -> Dict[str, Any]:
62
- """Collect the four accessibility prop keys into a dict.
47
+ def _make_element(
48
+ name: str,
49
+ *children: Element,
50
+ style: StyleProp = None,
51
+ ref: Optional[Dict[str, Any]] = None,
52
+ key: Optional[str] = None,
53
+ _defaults: Optional[Dict[str, Any]] = None,
54
+ _forced: Optional[Dict[str, Any]] = None,
55
+ **props: Any,
56
+ ) -> Element:
57
+ """Build an [`Element`][pythonnative.Element] of type ``name``.
58
+
59
+ This is the single helper every built-in factory routes through, so
60
+ the cross-cutting concerns that used to be duplicated per component
61
+ live in one place:
62
+
63
+ 1. ``style`` is flattened via
64
+ [`resolve_style`][pythonnative.style.resolve_style] (list-of-dicts
65
+ and ``None`` both handled).
66
+ 2. ``_defaults`` are filled in for keys not already present (used for
67
+ things like ``View``'s default ``flex_direction: "column"`` that
68
+ a user style may legitimately override).
69
+ 3. ``**props`` are merged on top, with ``None`` values *dropped* so
70
+ optional kwargs don't pollute the prop dict.
71
+ 4. ``ref`` is attached under the reserved ``"ref"`` key.
72
+ 5. ``_forced`` overrides everything (used by ``Column`` / ``Row`` to
73
+ lock their flex direction regardless of user style).
63
74
 
64
- Internal helper kept here so every component factory can expose
65
- the same four kwargs without repeating the ``if x is not None``
66
- plumbing. Returns an empty dict when no accessibility values are
67
- supplied so we don't bloat element props.
75
+ Args:
76
+ name: Element type name (e.g. ``"Text"``).
77
+ *children: Child elements.
78
+ style: Style dict, list of dicts, or ``None``.
79
+ ref: Optional ``use_ref()`` dict; the reconciler populates
80
+ ``ref["current"]`` with the underlying native view.
81
+ key: Stable identity for keyed reconciliation.
82
+ _defaults: Internal: fill-only-if-missing prop defaults.
83
+ _forced: Internal: prop overrides applied last.
84
+ **props: Per-component props. ``None`` values are dropped.
85
+
86
+ Returns:
87
+ A fresh [`Element`][pythonnative.Element].
68
88
  """
69
- out: Dict[str, Any] = {}
70
- if accessibility_label is not None:
71
- out["accessibility_label"] = accessibility_label
72
- if accessibility_hint is not None:
73
- out["accessibility_hint"] = accessibility_hint
74
- if accessibility_role is not None:
75
- out["accessibility_role"] = accessibility_role
76
- if accessible is not None:
77
- out["accessible"] = accessible
78
- return out
89
+ out: Dict[str, Any] = dict(resolve_style(style))
90
+ if _defaults:
91
+ for k, v in _defaults.items():
92
+ out.setdefault(k, v)
93
+ for k, v in props.items():
94
+ if v is not None:
95
+ out[k] = v
96
+ if ref is not None:
97
+ out["ref"] = ref
98
+ if _forced:
99
+ out.update(_forced)
100
+ return Element(name, out, list(children), key=key)
101
+
102
+
103
+ # ======================================================================
104
+ # Props dataclasses
105
+ # ======================================================================
106
+ #
107
+ # These are the canonical schemas for every built-in component. They
108
+ # subclass the SDK's ``Props`` base, so the same shape works for both
109
+ # the built-in factory functions and the third-party
110
+ # [`element_factory`][pythonnative.element_factory] API.
111
+
112
+
113
+ @dataclass(frozen=True)
114
+ class TextProps(Props):
115
+ """Props for [`Text`][pythonnative.Text]."""
116
+
117
+ text: str = ""
118
+ accessibility_label: Optional[str] = None
119
+ accessibility_hint: Optional[str] = None
120
+ accessibility_role: Optional[str] = None
121
+ accessible: Optional[bool] = None
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class ButtonProps(Props):
126
+ """Props for [`Button`][pythonnative.Button]."""
127
+
128
+ title: str = ""
129
+ on_click: Optional[Callable[[], None]] = None
130
+ enabled: bool = True
131
+ accessibility_label: Optional[str] = None
132
+ accessibility_hint: Optional[str] = None
133
+ accessibility_role: Optional[str] = None
134
+ accessible: Optional[bool] = None
135
+
136
+
137
+ @dataclass(frozen=True)
138
+ class TextInputProps(Props):
139
+ """Props for [`TextInput`][pythonnative.TextInput]."""
140
+
141
+ value: str = ""
142
+ placeholder: Optional[str] = None
143
+ on_change: Optional[Callable[[str], None]] = None
144
+ on_submit: Optional[Callable[[str], None]] = None
145
+ secure: bool = False
146
+ multiline: bool = False
147
+ keyboard_type: Optional[KeyboardType] = None
148
+ auto_capitalize: Optional[AutoCapitalize] = None
149
+ auto_correct: Optional[bool] = None
150
+ auto_focus: bool = False
151
+ return_key_type: Optional[ReturnKeyType] = None
152
+ max_length: Optional[int] = None
153
+ placeholder_color: Optional[Color] = None
154
+ accessibility_label: Optional[str] = None
155
+ accessibility_hint: Optional[str] = None
156
+ accessible: Optional[bool] = None
157
+
158
+
159
+ @dataclass(frozen=True)
160
+ class ImageProps(Props):
161
+ """Props for [`Image`][pythonnative.Image]."""
162
+
163
+ source: Optional[str] = None
164
+ scale_type: Optional[ScaleType] = None
165
+ tint_color: Optional[Color] = None
166
+ accessibility_label: Optional[str] = None
167
+ accessibility_role: Optional[str] = None
168
+ accessible: Optional[bool] = None
169
+
170
+
171
+ @dataclass(frozen=True)
172
+ class SwitchProps(Props):
173
+ """Props for [`Switch`][pythonnative.Switch]."""
174
+
175
+ value: bool = False
176
+ on_change: Optional[Callable[[bool], None]] = None
177
+
178
+
179
+ @dataclass(frozen=True)
180
+ class ProgressBarProps(Props):
181
+ """Props for [`ProgressBar`][pythonnative.ProgressBar]."""
182
+
183
+ value: float = 0.0
184
+
185
+
186
+ @dataclass(frozen=True)
187
+ class ActivityIndicatorProps(Props):
188
+ """Props for [`ActivityIndicator`][pythonnative.ActivityIndicator]."""
189
+
190
+ animating: bool = True
191
+
192
+
193
+ @dataclass(frozen=True)
194
+ class WebViewProps(Props):
195
+ """Props for [`WebView`][pythonnative.WebView]."""
196
+
197
+ url: Optional[str] = None
198
+
199
+
200
+ @dataclass(frozen=True)
201
+ class SpacerProps(Props):
202
+ """Props for [`Spacer`][pythonnative.Spacer]."""
203
+
204
+ size: Optional[float] = None
205
+ flex: Optional[float] = None
206
+
207
+
208
+ @dataclass(frozen=True)
209
+ class SliderProps(Props):
210
+ """Props for [`Slider`][pythonnative.Slider]."""
211
+
212
+ value: float = 0.0
213
+ min_value: float = 0.0
214
+ max_value: float = 1.0
215
+ on_change: Optional[Callable[[float], None]] = None
216
+
217
+
218
+ @dataclass(frozen=True)
219
+ class ViewProps(Props):
220
+ """Props for [`View`][pythonnative.View], [`Column`][pythonnative.Column], and [`Row`][pythonnative.Row]."""
221
+
222
+ accessibility_label: Optional[str] = None
223
+ accessibility_hint: Optional[str] = None
224
+ accessibility_role: Optional[str] = None
225
+ accessible: Optional[bool] = None
226
+
227
+
228
+ @dataclass(frozen=True)
229
+ class ScrollViewProps(Props):
230
+ """Props for [`ScrollView`][pythonnative.ScrollView]."""
231
+
232
+ refresh_control: Optional[Dict[str, Any]] = None
233
+ scroll_axis: Optional[Literal["vertical", "horizontal"]] = None
234
+
235
+
236
+ @dataclass(frozen=True)
237
+ class SafeAreaViewProps(Props):
238
+ """Props for [`SafeAreaView`][pythonnative.SafeAreaView]."""
239
+
240
+
241
+ @dataclass(frozen=True)
242
+ class ModalProps(Props):
243
+ """Props for [`Modal`][pythonnative.Modal]."""
244
+
245
+ visible: bool = False
246
+ on_dismiss: Optional[Callable[[], None]] = None
247
+ title: Optional[str] = None
248
+ animation_type: Literal["slide", "fade", "none"] = "slide"
249
+ transparent: bool = False
250
+
251
+
252
+ @dataclass(frozen=True)
253
+ class PressableProps(Props):
254
+ """Props for [`Pressable`][pythonnative.Pressable]."""
255
+
256
+ on_press: Optional[Callable[[], None]] = None
257
+ on_long_press: Optional[Callable[[], None]] = None
258
+ pressed_opacity: float = 0.6
259
+ accessibility_label: Optional[str] = None
260
+ accessibility_hint: Optional[str] = None
261
+ accessibility_role: Optional[str] = None
262
+ accessible: Optional[bool] = None
263
+
264
+
265
+ @dataclass(frozen=True)
266
+ class StatusBarProps(Props):
267
+ """Props for [`StatusBar`][pythonnative.StatusBar]."""
268
+
269
+ bar_style: Optional[Literal["light", "dark", "default"]] = None
270
+ background_color: Optional[Color] = None
271
+ hidden: Optional[bool] = None
272
+
273
+
274
+ @dataclass(frozen=True)
275
+ class KeyboardAvoidingViewProps(Props):
276
+ """Props for [`KeyboardAvoidingView`][pythonnative.KeyboardAvoidingView]."""
277
+
278
+ behavior: Literal["padding", "position"] = "padding"
279
+
280
+
281
+ @dataclass(frozen=True)
282
+ class PickerProps(Props):
283
+ """Props for [`Picker`][pythonnative.Picker].
284
+
285
+ ``items`` is an ordered list of ``{"value": Any, "label": str}``
286
+ entries. ``value`` is matched against ``items[i]["value"]`` to
287
+ determine the currently selected row.
288
+ """
289
+
290
+ value: Any = None
291
+ items: List[Dict[str, Any]] = field(default_factory=list)
292
+ on_change: Optional[Callable[[Any], None]] = None
293
+ placeholder: str = "Select…"
294
+ accessibility_label: Optional[str] = None
295
+ accessibility_hint: Optional[str] = None
296
+ accessible: Optional[bool] = None
297
+
298
+
299
+ # ======================================================================
300
+ # Leaf factories
301
+ # ======================================================================
79
302
 
80
303
 
81
304
  def Text(
@@ -91,34 +314,38 @@ def Text(
91
314
  ) -> Element:
92
315
  """Display a string of text.
93
316
 
94
- Style properties: `font_size`, `color`, `bold`, `font_weight`,
95
- `font_family`, `italic`, `text_align`, `background_color`,
96
- `max_lines`, `letter_spacing`, `line_height`, `text_decoration`
97
- (`"underline"` / `"line_through"`), `border_radius`,
98
- `border_width`, `border_color`, `shadow_*`, `opacity`,
99
- `transform`, plus the common layout props.
317
+ Style properties: ``font_size``, ``color``, ``bold``,
318
+ ``font_weight``, ``font_family``, ``italic``, ``text_align``,
319
+ ``background_color``, ``max_lines``, ``letter_spacing``,
320
+ ``line_height``, ``text_decoration`` (``"underline"`` /
321
+ ``"line_through"``), ``border_radius``, ``border_width``,
322
+ ``border_color``, ``shadow_*``, ``opacity``, ``transform``, plus
323
+ the common layout props.
100
324
 
101
325
  Args:
102
326
  text: Text content to display.
103
- style: Style dict (or list of dicts) controlling appearance and
104
- layout.
327
+ style: Style dict (or list of dicts).
105
328
  accessibility_label: Spoken description for screen readers.
106
329
  accessibility_hint: Spoken extra detail (iOS only).
107
330
  accessibility_role: Semantic role for assistive tech.
108
331
  accessible: Override whether the element is exposed to AT.
109
- ref: Optional ``use_ref()`` dict; the reconciler populates
110
- ``ref["current"]`` with the underlying native view.
111
- key: Stable identity for keyed reconciliation in lists.
332
+ ref: Optional ``use_ref()`` dict.
333
+ key: Stable identity for keyed reconciliation.
112
334
 
113
335
  Returns:
114
- An [`Element`][pythonnative.Element] of type `"Text"`.
336
+ An [`Element`][pythonnative.Element] of type ``"Text"``.
115
337
  """
116
- props: Dict[str, Any] = {"text": text}
117
- props.update(resolve_style(style))
118
- props.update(_accessibility_props(accessibility_label, accessibility_hint, accessibility_role, accessible))
119
- if ref is not None:
120
- props["ref"] = ref
121
- return Element("Text", props, [], key=key)
338
+ return _make_element(
339
+ "Text",
340
+ style=style,
341
+ ref=ref,
342
+ key=key,
343
+ text=text,
344
+ accessibility_label=accessibility_label,
345
+ accessibility_hint=accessibility_hint,
346
+ accessibility_role=accessibility_role,
347
+ accessible=accessible,
348
+ )
122
349
 
123
350
 
124
351
  def Button(
@@ -129,55 +356,55 @@ def Button(
129
356
  style: StyleProp = None,
130
357
  accessibility_label: Optional[str] = None,
131
358
  accessibility_hint: Optional[str] = None,
359
+ accessibility_role: Optional[str] = None,
132
360
  accessible: Optional[bool] = None,
133
361
  ref: Optional[Dict[str, Any]] = None,
134
362
  key: Optional[str] = None,
135
363
  ) -> Element:
136
364
  """Display a tappable button.
137
365
 
138
- Style properties: `color`, `background_color`, `font_size`,
139
- `border_radius`, `border_width`, `border_color`, `shadow_*`,
140
- `opacity`, `transform`, plus the common layout props.
366
+ Style properties: ``color``, ``background_color``, ``font_size``,
367
+ ``border_radius``, ``border_width``, ``border_color``, ``shadow_*``,
368
+ ``opacity``, ``transform``, plus the common layout props.
369
+
370
+ Buttons get ``accessibility_role="button"`` by default.
141
371
 
142
372
  Args:
143
373
  title: Button label.
144
374
  on_click: Callback invoked when the user taps the button.
145
- enabled: When `False`, the button is disabled and cannot be
375
+ enabled: When ``False``, the button is disabled and cannot be
146
376
  tapped.
147
377
  style: Style dict (or list of dicts).
148
378
  accessibility_label: Spoken description for screen readers.
149
379
  accessibility_hint: Spoken extra detail (iOS only).
380
+ accessibility_role: Override the default ``"button"`` role.
150
381
  accessible: Override whether the element is exposed to AT.
151
- ref: Optional ``use_ref()`` dict; the reconciler populates
152
- ``ref["current"]`` with the underlying native view.
382
+ ref: Optional ``use_ref()`` dict.
153
383
  key: Stable identity for keyed reconciliation.
154
384
 
155
385
  Returns:
156
- An [`Element`][pythonnative.Element] of type `"Button"`.
386
+ An [`Element`][pythonnative.Element] of type ``"Button"``.
157
387
  """
158
- props: Dict[str, Any] = {"title": title}
159
- if on_click is not None:
160
- props["on_click"] = on_click
161
- if not enabled:
162
- props["enabled"] = False
163
- props.update(resolve_style(style))
164
- # Buttons get accessibility_role="button" by default.
165
- if accessibility_label is not None:
166
- props["accessibility_label"] = accessibility_label
167
- if accessibility_hint is not None:
168
- props["accessibility_hint"] = accessibility_hint
169
- if accessible is not None:
170
- props["accessible"] = accessible
171
- props.setdefault("accessibility_role", "button")
172
- if ref is not None:
173
- props["ref"] = ref
174
- return Element("Button", props, [], key=key)
388
+ return _make_element(
389
+ "Button",
390
+ style=style,
391
+ ref=ref,
392
+ key=key,
393
+ title=title,
394
+ on_click=on_click,
395
+ enabled=enabled,
396
+ accessibility_label=accessibility_label,
397
+ accessibility_hint=accessibility_hint,
398
+ accessibility_role=accessibility_role,
399
+ accessible=accessible,
400
+ _defaults={"accessibility_role": "button"},
401
+ )
175
402
 
176
403
 
177
404
  def TextInput(
178
405
  *,
179
406
  value: str = "",
180
- placeholder: str = "",
407
+ placeholder: Optional[str] = None,
181
408
  on_change: Optional[Callable[[str], None]] = None,
182
409
  on_submit: Optional[Callable[[str], None]] = None,
183
410
  secure: bool = False,
@@ -196,30 +423,29 @@ def TextInput(
196
423
  ref: Optional[Dict[str, Any]] = None,
197
424
  key: Optional[str] = None,
198
425
  ) -> Element:
199
- """Display a text entry field (single-line by default, or `multiline`).
426
+ """Display a text-entry field (single-line by default, or ``multiline``).
200
427
 
201
- Style properties: `font_size`, `color`, `background_color`,
202
- `border_*`, plus the common layout props.
428
+ Style properties: ``font_size``, ``color``, ``background_color``,
429
+ ``border_*``, plus the common layout props.
203
430
 
204
431
  Args:
205
432
  value: Current text content (controlled-input pattern).
206
- placeholder: Hint shown when `value` is empty.
433
+ placeholder: Hint shown when ``value`` is empty.
207
434
  on_change: Callback invoked with the new string each keystroke.
208
435
  on_submit: Callback invoked when the user submits (Return /
209
436
  Done / etc.). Receives the final text.
210
- secure: When `True`, characters are masked (use for passwords).
211
- multiline: When `True`, allows multiple lines of input.
437
+ secure: When ``True``, characters are masked (use for passwords).
438
+ multiline: When ``True``, allows multiple lines of input.
212
439
  keyboard_type: One of ``"default"``, ``"email_address"``,
213
- ``"number_pad"``, ``"decimal_pad"``, ``"phone_pad"``,
214
- ``"url"``.
215
- auto_capitalize: One of ``"none"``, ``"sentences"``,
216
- ``"words"``, ``"characters"``.
440
+ ``"number_pad"``, ``"decimal_pad"``, ``"phone_pad"``, ``"url"``.
441
+ auto_capitalize: One of ``"none"``, ``"sentences"``, ``"words"``,
442
+ ``"characters"``.
217
443
  auto_correct: Enable/disable autocorrection.
218
444
  auto_focus: Request focus on mount.
219
445
  return_key_type: One of ``"default"``, ``"done"``, ``"go"``,
220
446
  ``"next"``, ``"send"``, ``"search"``.
221
447
  max_length: Maximum number of characters allowed.
222
- placeholder_color: Color to use for the placeholder string.
448
+ placeholder_color: Color used for the placeholder string.
223
449
  style: Style dict (or list of dicts).
224
450
  accessibility_label: Spoken description for screen readers.
225
451
  accessibility_hint: Spoken extra detail (iOS only).
@@ -228,38 +454,30 @@ def TextInput(
228
454
  key: Stable identity for keyed reconciliation.
229
455
 
230
456
  Returns:
231
- An [`Element`][pythonnative.Element] of type `"TextInput"`.
457
+ An [`Element`][pythonnative.Element] of type ``"TextInput"``.
232
458
  """
233
- props: Dict[str, Any] = {"value": value}
234
- if placeholder:
235
- props["placeholder"] = placeholder
236
- if on_change is not None:
237
- props["on_change"] = on_change
238
- if on_submit is not None:
239
- props["on_submit"] = on_submit
240
- if secure:
241
- props["secure"] = True
242
- if multiline:
243
- props["multiline"] = True
244
- if keyboard_type is not None:
245
- props["keyboard_type"] = keyboard_type
246
- if auto_capitalize is not None:
247
- props["auto_capitalize"] = auto_capitalize
248
- if auto_correct is not None:
249
- props["auto_correct"] = auto_correct
250
- if auto_focus:
251
- props["auto_focus"] = True
252
- if return_key_type is not None:
253
- props["return_key_type"] = return_key_type
254
- if max_length is not None:
255
- props["max_length"] = max_length
256
- if placeholder_color is not None:
257
- props["placeholder_color"] = placeholder_color
258
- props.update(resolve_style(style))
259
- props.update(_accessibility_props(accessibility_label, accessibility_hint, None, accessible))
260
- if ref is not None:
261
- props["ref"] = ref
262
- return Element("TextInput", props, [], key=key)
459
+ return _make_element(
460
+ "TextInput",
461
+ style=style,
462
+ ref=ref,
463
+ key=key,
464
+ value=value,
465
+ placeholder=placeholder,
466
+ on_change=on_change,
467
+ on_submit=on_submit,
468
+ secure=secure or None,
469
+ multiline=multiline or None,
470
+ keyboard_type=keyboard_type,
471
+ auto_capitalize=auto_capitalize,
472
+ auto_correct=auto_correct,
473
+ auto_focus=auto_focus or None,
474
+ return_key_type=return_key_type,
475
+ max_length=max_length,
476
+ placeholder_color=placeholder_color,
477
+ accessibility_label=accessibility_label,
478
+ accessibility_hint=accessibility_hint,
479
+ accessible=accessible,
480
+ )
263
481
 
264
482
 
265
483
  def Image(
@@ -269,46 +487,50 @@ def Image(
269
487
  tint_color: Optional[Color] = None,
270
488
  style: StyleProp = None,
271
489
  accessibility_label: Optional[str] = None,
490
+ accessibility_role: Optional[str] = None,
272
491
  accessible: Optional[bool] = None,
273
492
  ref: Optional[Dict[str, Any]] = None,
274
493
  key: Optional[str] = None,
275
494
  ) -> Element:
276
495
  """Display an image from a resource path or URL.
277
496
 
278
- Style properties: `background_color`, `border_*`, `opacity`,
279
- `transform`, plus the common layout props.
497
+ Style properties: ``background_color``, ``border_*``, ``opacity``,
498
+ ``transform``, plus the common layout props.
280
499
 
281
500
  Network images (``http://`` / ``https://``) are loaded
282
- asynchronously off the main thread on both iOS (via NSURLSession)
283
- and Android (via a worker thread + `BitmapFactory`).
501
+ asynchronously off the main thread on both iOS (via
502
+ ``NSURLSession``) and Android (via a worker thread plus
503
+ ``BitmapFactory``).
284
504
 
285
505
  Args:
286
506
  source: Image resource name or URL.
287
- scale_type: Fit mode: `"cover"`, `"contain"`, `"stretch"`,
288
- `"center"`.
507
+ scale_type: Fit mode: ``"cover"``, ``"contain"``, ``"stretch"``,
508
+ ``"center"``.
289
509
  tint_color: Color overlay applied to template images
290
510
  (monochrome icons).
291
511
  style: Style dict (or list of dicts).
292
512
  accessibility_label: Spoken description for screen readers.
513
+ accessibility_role: Override the default ``"image"`` role.
293
514
  accessible: Override whether the element is exposed to AT.
294
515
  ref: Optional ``use_ref()`` dict.
295
516
  key: Stable identity for keyed reconciliation.
296
517
 
297
518
  Returns:
298
- An [`Element`][pythonnative.Element] of type `"Image"`.
519
+ An [`Element`][pythonnative.Element] of type ``"Image"``.
299
520
  """
300
- props: Dict[str, Any] = {}
301
- if source:
302
- props["source"] = source
303
- if scale_type is not None:
304
- props["scale_type"] = scale_type
305
- if tint_color is not None:
306
- props["tint_color"] = tint_color
307
- props.update(resolve_style(style))
308
- props.update(_accessibility_props(accessibility_label, None, "image", accessible))
309
- if ref is not None:
310
- props["ref"] = ref
311
- return Element("Image", props, [], key=key)
521
+ return _make_element(
522
+ "Image",
523
+ style=style,
524
+ ref=ref,
525
+ key=key,
526
+ source=source or None,
527
+ scale_type=scale_type,
528
+ tint_color=tint_color,
529
+ accessibility_label=accessibility_label,
530
+ accessibility_role=accessibility_role,
531
+ accessible=accessible,
532
+ _defaults={"accessibility_role": "image"},
533
+ )
312
534
 
313
535
 
314
536
  def Switch(
@@ -327,13 +549,15 @@ def Switch(
327
549
  key: Stable identity for keyed reconciliation.
328
550
 
329
551
  Returns:
330
- An [`Element`][pythonnative.Element] of type `"Switch"`.
552
+ An [`Element`][pythonnative.Element] of type ``"Switch"``.
331
553
  """
332
- props: Dict[str, Any] = {"value": value}
333
- if on_change is not None:
334
- props["on_change"] = on_change
335
- props.update(resolve_style(style))
336
- return Element("Switch", props, [], key=key)
554
+ return _make_element(
555
+ "Switch",
556
+ style=style,
557
+ key=key,
558
+ value=value,
559
+ on_change=on_change,
560
+ )
337
561
 
338
562
 
339
563
  def ProgressBar(
@@ -342,23 +566,26 @@ def ProgressBar(
342
566
  style: StyleProp = None,
343
567
  key: Optional[str] = None,
344
568
  ) -> Element:
345
- """Show determinate progress as a value between 0.0 and 1.0.
569
+ """Show determinate progress as a value between ``0.0`` and ``1.0``.
346
570
 
347
571
  For indeterminate progress, use
348
572
  [`ActivityIndicator`][pythonnative.ActivityIndicator] instead.
349
573
 
350
574
  Args:
351
- value: Fraction complete (clamped to `[0.0, 1.0]` by the
575
+ value: Fraction complete (clamped to ``[0.0, 1.0]`` by the
352
576
  platform handler).
353
577
  style: Style dict (or list of dicts).
354
578
  key: Stable identity for keyed reconciliation.
355
579
 
356
580
  Returns:
357
- An [`Element`][pythonnative.Element] of type `"ProgressBar"`.
581
+ An [`Element`][pythonnative.Element] of type ``"ProgressBar"``.
358
582
  """
359
- props: Dict[str, Any] = {"value": value}
360
- props.update(resolve_style(style))
361
- return Element("ProgressBar", props, [], key=key)
583
+ return _make_element(
584
+ "ProgressBar",
585
+ style=style,
586
+ key=key,
587
+ value=value,
588
+ )
362
589
 
363
590
 
364
591
  def ActivityIndicator(
@@ -370,17 +597,20 @@ def ActivityIndicator(
370
597
  """Show an indeterminate loading spinner.
371
598
 
372
599
  Args:
373
- animating: When `False`, the spinner is hidden.
600
+ animating: When ``False``, the spinner is hidden.
374
601
  style: Style dict (or list of dicts).
375
602
  key: Stable identity for keyed reconciliation.
376
603
 
377
604
  Returns:
378
605
  An [`Element`][pythonnative.Element] of type
379
- `"ActivityIndicator"`.
606
+ ``"ActivityIndicator"``.
380
607
  """
381
- props: Dict[str, Any] = {"animating": animating}
382
- props.update(resolve_style(style))
383
- return Element("ActivityIndicator", props, [], key=key)
608
+ return _make_element(
609
+ "ActivityIndicator",
610
+ style=style,
611
+ key=key,
612
+ animating=animating,
613
+ )
384
614
 
385
615
 
386
616
  def WebView(
@@ -397,13 +627,14 @@ def WebView(
397
627
  key: Stable identity for keyed reconciliation.
398
628
 
399
629
  Returns:
400
- An [`Element`][pythonnative.Element] of type `"WebView"`.
630
+ An [`Element`][pythonnative.Element] of type ``"WebView"``.
401
631
  """
402
- props: Dict[str, Any] = {}
403
- if url:
404
- props["url"] = url
405
- props.update(resolve_style(style))
406
- return Element("WebView", props, [], key=key)
632
+ return _make_element(
633
+ "WebView",
634
+ style=style,
635
+ key=key,
636
+ url=url or None,
637
+ )
407
638
 
408
639
 
409
640
  def Spacer(
@@ -414,32 +645,31 @@ def Spacer(
414
645
  ) -> Element:
415
646
  """Insert empty space inside a flex container.
416
647
 
417
- Pass `size` for a fixed gap, or `flex` to expand and absorb
648
+ Pass ``size`` for a fixed gap, or ``flex`` to expand and absorb
418
649
  remaining space.
419
650
 
420
651
  Args:
421
- size: Fixed gap in dp/pt along the parent's main axis.
652
+ size: Fixed gap in dp/pt along the parent's main axis. Mirrored
653
+ on both axes — whichever axis the parent's
654
+ ``flex_direction`` chooses as main becomes the actual gap.
422
655
  flex: Flex-grow weight; useful for pushing siblings to the
423
656
  opposite end of a [`Row`][pythonnative.Row] or
424
657
  [`Column`][pythonnative.Column].
425
658
  key: Stable identity for keyed reconciliation.
426
659
 
427
660
  Returns:
428
- An [`Element`][pythonnative.Element] of type `"Spacer"`.
661
+ An [`Element`][pythonnative.Element] of type ``"Spacer"``.
429
662
  """
430
- props: Dict[str, Any] = {}
431
- if size is not None:
432
- # The layout engine sees ``width`` / ``height`` only, so a fixed
433
- # ``size`` is mirrored on both axes. Whichever axis the parent
434
- # container's ``flex_direction`` chooses as main becomes the
435
- # actual gap; the cross axis is constrained by the parent's
436
- # ``align_items`` (typically ``stretch``) anyway.
437
- props["size"] = size
438
- props["width"] = size
439
- props["height"] = size
440
- if flex is not None:
441
- props["flex"] = flex
442
- return Element("Spacer", props, [], key=key)
663
+ width = size if size is not None else None
664
+ height = size if size is not None else None
665
+ return _make_element(
666
+ "Spacer",
667
+ key=key,
668
+ size=size,
669
+ width=width,
670
+ height=height,
671
+ flex=flex,
672
+ )
443
673
 
444
674
 
445
675
  def Slider(
@@ -451,7 +681,7 @@ def Slider(
451
681
  style: StyleProp = None,
452
682
  key: Optional[str] = None,
453
683
  ) -> Element:
454
- """Continuous-value slider between `min_value` and `max_value`.
684
+ """Continuous-value slider between ``min_value`` and ``max_value``.
455
685
 
456
686
  Args:
457
687
  value: Current slider value.
@@ -463,21 +693,21 @@ def Slider(
463
693
  key: Stable identity for keyed reconciliation.
464
694
 
465
695
  Returns:
466
- An [`Element`][pythonnative.Element] of type `"Slider"`.
696
+ An [`Element`][pythonnative.Element] of type ``"Slider"``.
467
697
  """
468
- props: Dict[str, Any] = {
469
- "value": value,
470
- "min_value": min_value,
471
- "max_value": max_value,
472
- }
473
- if on_change is not None:
474
- props["on_change"] = on_change
475
- props.update(resolve_style(style))
476
- return Element("Slider", props, [], key=key)
698
+ return _make_element(
699
+ "Slider",
700
+ style=style,
701
+ key=key,
702
+ value=value,
703
+ min_value=min_value,
704
+ max_value=max_value,
705
+ on_change=on_change,
706
+ )
477
707
 
478
708
 
479
709
  # ======================================================================
480
- # Container components
710
+ # Container factories
481
711
  # ======================================================================
482
712
 
483
713
 
@@ -491,28 +721,24 @@ def View(
491
721
  ref: Optional[Dict[str, Any]] = None,
492
722
  key: Optional[str] = None,
493
723
  ) -> Element:
494
- """Universal flex container (like React Native's `View`).
724
+ """Universal flex container (like React Native's ``View``).
495
725
 
496
- Defaults to `flex_direction: "column"`. Override via `style`:
726
+ Defaults to ``flex_direction: "column"`` (override via ``style``).
497
727
 
498
- ```python
499
- pn.View(child_a, child_b, style={"flex_direction": "row"})
500
- ```
728
+ Flex container properties (passed via ``style``):
501
729
 
502
- Flex container properties (inside `style`):
503
-
504
- - `flex_direction`: `"column"` (default), `"row"`,
505
- `"column_reverse"`, `"row_reverse"`.
506
- - `justify_content`: main-axis distribution. Accepts `"flex_start"`
507
- (default), `"center"`, `"flex_end"`, `"space_between"`,
508
- `"space_around"`, `"space_evenly"`.
509
- - `align_items`: cross-axis alignment. Accepts `"stretch"` (default),
510
- `"flex_start"`, `"center"`, `"flex_end"`.
511
- - `overflow`: `"visible"` (default) or `"hidden"`.
512
- - `spacing`, `padding`, `background_color`, `border_radius`,
513
- `border_width`, `border_color`, `shadow_color`, `shadow_offset`,
514
- `shadow_opacity`, `shadow_radius`, `elevation`, `opacity`,
515
- `transform`.
730
+ - ``flex_direction``: ``"column"`` (default), ``"row"``,
731
+ ``"column_reverse"``, ``"row_reverse"``.
732
+ - ``justify_content``: main-axis distribution. Accepts
733
+ ``"flex_start"`` (default), ``"center"``, ``"flex_end"``,
734
+ ``"space_between"``, ``"space_around"``, ``"space_evenly"``.
735
+ - ``align_items``: cross-axis alignment. Accepts ``"stretch"``
736
+ (default), ``"flex_start"``, ``"center"``, ``"flex_end"``.
737
+ - ``overflow``: ``"visible"`` (default) or ``"hidden"``.
738
+ - ``spacing``, ``padding``, ``background_color``, ``border_radius``,
739
+ ``border_width``, ``border_color``, ``shadow_color``,
740
+ ``shadow_offset``, ``shadow_opacity``, ``shadow_radius``,
741
+ ``elevation``, ``opacity``, ``transform``.
516
742
 
517
743
  Args:
518
744
  *children: Child elements rendered inside the container.
@@ -521,19 +747,24 @@ def View(
521
747
  accessibility_hint: Spoken extra detail (iOS only).
522
748
  accessibility_role: Semantic role for assistive tech.
523
749
  accessible: Override whether the element is exposed to AT.
524
- ref: Optional ``use_ref()`` dict; the reconciler populates
525
- ``ref["current"]`` with the underlying native view.
750
+ ref: Optional ``use_ref()`` dict.
526
751
  key: Stable identity for keyed reconciliation.
527
752
 
528
753
  Returns:
529
- An [`Element`][pythonnative.Element] of type `"View"`.
754
+ An [`Element`][pythonnative.Element] of type ``"View"``.
530
755
  """
531
- props: Dict[str, Any] = {"flex_direction": "column"}
532
- props.update(resolve_style(style))
533
- props.update(_accessibility_props(accessibility_label, accessibility_hint, accessibility_role, accessible))
534
- if ref is not None:
535
- props["ref"] = ref
536
- return Element("View", props, list(children), key=key)
756
+ return _make_element(
757
+ "View",
758
+ *children,
759
+ style=style,
760
+ ref=ref,
761
+ key=key,
762
+ accessibility_label=accessibility_label,
763
+ accessibility_hint=accessibility_hint,
764
+ accessibility_role=accessibility_role,
765
+ accessible=accessible,
766
+ _defaults={"flex_direction": "column"},
767
+ )
537
768
 
538
769
 
539
770
  def Column(
@@ -545,20 +776,8 @@ def Column(
545
776
  """Arrange children vertically.
546
777
 
547
778
  Convenience wrapper around [`View`][pythonnative.View] with
548
- `flex_direction` fixed to `"column"`. Use `View` directly if you
549
- need to switch between row and column at runtime.
550
-
551
- Style properties: `spacing`, `padding`, `align_items`,
552
- `justify_content`, `background_color`, `overflow`, plus the common
553
- layout props.
554
-
555
- `align_items` controls cross-axis (horizontal) alignment:
556
- `"stretch"` (default), `"flex_start"` / `"leading"`, `"center"`, or
557
- `"flex_end"` / `"trailing"`.
558
-
559
- `justify_content` controls main-axis (vertical) distribution:
560
- `"flex_start"` (default), `"center"`, `"flex_end"`,
561
- `"space_between"`, `"space_around"`, `"space_evenly"`.
779
+ ``flex_direction`` locked to ``"column"``. Use ``View`` directly if
780
+ you need to switch between row and column at runtime.
562
781
 
563
782
  Args:
564
783
  *children: Child elements stacked top to bottom.
@@ -567,14 +786,16 @@ def Column(
567
786
  key: Stable identity for keyed reconciliation.
568
787
 
569
788
  Returns:
570
- An [`Element`][pythonnative.Element] of type `"Column"`.
789
+ An [`Element`][pythonnative.Element] of type ``"Column"``.
571
790
  """
572
- props: Dict[str, Any] = {"flex_direction": "column"}
573
- props.update(resolve_style(style))
574
- props["flex_direction"] = "column"
575
- if ref is not None:
576
- props["ref"] = ref
577
- return Element("Column", props, list(children), key=key)
791
+ return _make_element(
792
+ "Column",
793
+ *children,
794
+ style=style,
795
+ ref=ref,
796
+ key=key,
797
+ _forced={"flex_direction": "column"},
798
+ )
578
799
 
579
800
 
580
801
  def Row(
@@ -586,20 +807,8 @@ def Row(
586
807
  """Arrange children horizontally.
587
808
 
588
809
  Convenience wrapper around [`View`][pythonnative.View] with
589
- `flex_direction` fixed to `"row"`. Use `View` directly if you need
590
- to switch between row and column at runtime.
591
-
592
- Style properties: `spacing`, `padding`, `align_items`,
593
- `justify_content`, `background_color`, `overflow`, plus the common
594
- layout props.
595
-
596
- `align_items` controls cross-axis (vertical) alignment:
597
- `"stretch"` (default), `"flex_start"` / `"top"`, `"center"`, or
598
- `"flex_end"` / `"bottom"`.
599
-
600
- `justify_content` controls main-axis (horizontal) distribution:
601
- `"flex_start"` (default), `"center"`, `"flex_end"`,
602
- `"space_between"`, `"space_around"`, `"space_evenly"`.
810
+ ``flex_direction`` locked to ``"row"``. Use ``View`` directly if you
811
+ need to switch between row and column at runtime.
603
812
 
604
813
  Args:
605
814
  *children: Child elements arranged left to right.
@@ -608,49 +817,57 @@ def Row(
608
817
  key: Stable identity for keyed reconciliation.
609
818
 
610
819
  Returns:
611
- An [`Element`][pythonnative.Element] of type `"Row"`.
820
+ An [`Element`][pythonnative.Element] of type ``"Row"``.
612
821
  """
613
- props: Dict[str, Any] = {"flex_direction": "row"}
614
- props.update(resolve_style(style))
615
- props["flex_direction"] = "row"
616
- if ref is not None:
617
- props["ref"] = ref
618
- return Element("Row", props, list(children), key=key)
822
+ return _make_element(
823
+ "Row",
824
+ *children,
825
+ style=style,
826
+ ref=ref,
827
+ key=key,
828
+ _forced={"flex_direction": "row"},
829
+ )
619
830
 
620
831
 
621
832
  def ScrollView(
622
- child: Optional[Element] = None,
623
- *,
833
+ *children: Element,
624
834
  refresh_control: Optional[Dict[str, Any]] = None,
835
+ scroll_axis: Optional[Literal["vertical", "horizontal"]] = None,
625
836
  style: StyleProp = None,
626
837
  ref: Optional[Dict[str, Any]] = None,
627
838
  key: Optional[str] = None,
628
839
  ) -> Element:
629
- """Wrap a single child in a scrollable container.
840
+ """Wrap children in a scrollable container.
841
+
842
+ ``ScrollView`` typically takes a single child (a ``Column`` or
843
+ ``Row`` aggregating the scrollable content). It accepts ``*children``
844
+ for ergonomic call sites; the underlying native scroll view stacks
845
+ them on its content axis.
630
846
 
631
847
  Args:
632
- child: The single child to scroll. Wrap multiple elements in a
633
- [`Column`][pythonnative.Column] or
634
- [`Row`][pythonnative.Row] first.
848
+ *children: Child elements to scroll.
635
849
  refresh_control: Optional pull-to-refresh spec, typically
636
850
  constructed via
637
851
  [`RefreshControl`][pythonnative.RefreshControl]. The dict
638
- must have ``refreshing`` (bool) and ``on_refresh`` (callable).
852
+ must have ``refreshing`` (bool) and ``on_refresh``
853
+ (callable).
854
+ scroll_axis: ``"vertical"`` (default) or ``"horizontal"``.
639
855
  style: Style dict (or list of dicts).
640
856
  ref: Optional ``use_ref()`` dict.
641
857
  key: Stable identity for keyed reconciliation.
642
858
 
643
859
  Returns:
644
- An [`Element`][pythonnative.Element] of type `"ScrollView"`.
860
+ An [`Element`][pythonnative.Element] of type ``"ScrollView"``.
645
861
  """
646
- children = [child] if child is not None else []
647
- props: Dict[str, Any] = {}
648
- if refresh_control is not None:
649
- props["refresh_control"] = refresh_control
650
- props.update(resolve_style(style))
651
- if ref is not None:
652
- props["ref"] = ref
653
- return Element("ScrollView", props, children, key=key)
862
+ return _make_element(
863
+ "ScrollView",
864
+ *children,
865
+ style=style,
866
+ ref=ref,
867
+ key=key,
868
+ refresh_control=refresh_control,
869
+ scroll_axis=scroll_axis,
870
+ )
654
871
 
655
872
 
656
873
  def SafeAreaView(
@@ -666,11 +883,14 @@ def SafeAreaView(
666
883
  key: Stable identity for keyed reconciliation.
667
884
 
668
885
  Returns:
669
- An [`Element`][pythonnative.Element] of type `"SafeAreaView"`.
886
+ An [`Element`][pythonnative.Element] of type ``"SafeAreaView"``.
670
887
  """
671
- props: Dict[str, Any] = {}
672
- props.update(resolve_style(style))
673
- return Element("SafeAreaView", props, list(children), key=key)
888
+ return _make_element(
889
+ "SafeAreaView",
890
+ *children,
891
+ style=style,
892
+ key=key,
893
+ )
674
894
 
675
895
 
676
896
  def Modal(
@@ -685,21 +905,21 @@ def Modal(
685
905
  ) -> Element:
686
906
  """Overlay modal dialog backed by a real native presentation.
687
907
 
688
- The modal is shown when `visible=True` and hidden when `False`.
689
- Drive `visible` from a hook so the parent component can dismiss
908
+ The modal is shown when ``visible=True`` and hidden when ``False``.
909
+ Drive ``visible`` from a hook so the parent component can dismiss
690
910
  the modal in response to user actions. On iOS this presents a
691
- `UIViewController`; on Android it shows an `android.app.Dialog`.
911
+ ``UIViewController``; on Android it shows an ``android.app.Dialog``.
692
912
 
693
913
  Children are mounted as the modal's content view, not into the
694
- on-tree placeholder, so they appear above all other native
695
- content and don't influence the underlying layout.
914
+ on-tree placeholder, so they appear above all other native content
915
+ and don't influence the underlying layout.
696
916
 
697
917
  Args:
698
918
  *children: Modal content.
699
919
  visible: Controls whether the modal is presented.
700
920
  on_dismiss: Callback invoked when the user dismisses the modal
701
- via system gesture (e.g., backdrop tap or back button).
702
- title: Optional title bar text.
921
+ via system gesture.
922
+ title: Optional title-bar text.
703
923
  animation_type: ``"slide"`` (default), ``"fade"``, or ``"none"``.
704
924
  transparent: When ``True``, the underlying view is dimmed
705
925
  instead of fully covered.
@@ -707,91 +927,152 @@ def Modal(
707
927
  key: Stable identity for keyed reconciliation.
708
928
 
709
929
  Returns:
710
- An [`Element`][pythonnative.Element] of type `"Modal"`.
930
+ An [`Element`][pythonnative.Element] of type ``"Modal"``.
711
931
  """
712
- props: Dict[str, Any] = {
713
- "visible": visible,
714
- "animation_type": animation_type,
715
- "transparent": transparent,
716
- }
717
- if on_dismiss is not None:
718
- props["on_dismiss"] = on_dismiss
719
- if title is not None:
720
- props["title"] = title
721
- props.update(resolve_style(style))
722
- return Element("Modal", props, list(children), key=key)
932
+ return _make_element(
933
+ "Modal",
934
+ *children,
935
+ style=style,
936
+ key=key,
937
+ visible=visible,
938
+ animation_type=animation_type,
939
+ transparent=transparent,
940
+ on_dismiss=on_dismiss,
941
+ title=title,
942
+ )
723
943
 
724
944
 
725
945
  def Pressable(
726
- child: Optional[Element] = None,
727
- *,
946
+ *children: Element,
728
947
  on_press: Optional[Callable[[], None]] = None,
729
948
  on_long_press: Optional[Callable[[], None]] = None,
730
949
  pressed_opacity: float = 0.6,
731
950
  style: StyleProp = None,
732
951
  accessibility_label: Optional[str] = None,
733
952
  accessibility_hint: Optional[str] = None,
953
+ accessibility_role: Optional[str] = None,
734
954
  accessible: Optional[bool] = None,
735
955
  key: Optional[str] = None,
736
956
  ) -> Element:
737
- """Wrap any child element with tap and long-press handlers.
957
+ """Wrap children with tap and long-press handlers.
738
958
 
739
959
  Useful for making non-button elements (text, images, custom views)
740
960
  respond to user taps. The wrapper view fades to ``pressed_opacity``
741
- on touch-down and back to full opacity on touch-up, providing
742
- subtle visual feedback (matches React Native's `Pressable` default).
961
+ on touch-down and back to full opacity on touch-up.
962
+
963
+ Pressable gets ``accessibility_role="button"`` by default.
743
964
 
744
965
  Args:
745
- child: The single element to make pressable.
966
+ *children: Elements to make pressable.
746
967
  on_press: Callback invoked on a normal tap.
747
968
  on_long_press: Callback invoked on a sustained press.
748
- pressed_opacity: Opacity (0-1) applied to the wrapper while
749
- the user's finger is down. Set to ``1.0`` for no visual
750
- feedback.
969
+ pressed_opacity: Opacity (01) applied while the user's finger
970
+ is down. Set to ``1.0`` for no visual feedback.
751
971
  style: Style dict applied to the wrapper.
752
972
  accessibility_label: Spoken description for screen readers.
753
973
  accessibility_hint: Spoken extra detail (iOS only).
974
+ accessibility_role: Override the default ``"button"`` role.
754
975
  accessible: Override whether the element is exposed to AT.
755
976
  key: Stable identity for keyed reconciliation.
756
977
 
757
978
  Returns:
758
- An [`Element`][pythonnative.Element] of type `"Pressable"`.
979
+ An [`Element`][pythonnative.Element] of type ``"Pressable"``.
759
980
  """
760
- props: Dict[str, Any] = {}
761
- if on_press is not None:
762
- props["on_press"] = on_press
763
- if on_long_press is not None:
764
- props["on_long_press"] = on_long_press
765
- if pressed_opacity != 0.6:
766
- props["pressed_opacity"] = pressed_opacity
767
- else:
768
- props.setdefault("pressed_opacity", 0.6)
769
- props.update(resolve_style(style))
770
- props.update(_accessibility_props(accessibility_label, accessibility_hint, "button", accessible))
771
- children = [child] if child is not None else []
772
- return Element("Pressable", props, children, key=key)
981
+ return _make_element(
982
+ "Pressable",
983
+ *children,
984
+ style=style,
985
+ key=key,
986
+ on_press=on_press,
987
+ on_long_press=on_long_press,
988
+ pressed_opacity=pressed_opacity,
989
+ accessibility_label=accessibility_label,
990
+ accessibility_hint=accessibility_hint,
991
+ accessibility_role=accessibility_role,
992
+ accessible=accessible,
993
+ _defaults={"accessibility_role": "button"},
994
+ )
995
+
996
+
997
+ # ======================================================================
998
+ # Fragment
999
+ # ======================================================================
1000
+
1001
+
1002
+ def Fragment(*children: Optional[Element], key: Optional[str] = None) -> Element:
1003
+ """Group children without adding a wrapping native view.
1004
+
1005
+ Like React's ``<></>``: returns multiple elements from a component
1006
+ without introducing an extra container. The reconciler flattens
1007
+ Fragment elements at the children-list level, so each child appears
1008
+ as a direct sibling of the Fragment's parent in the native tree.
1009
+
1010
+ Useful inside [`Provider`][pythonnative.Provider] /
1011
+ [`memo`][pythonnative.memo] / conditional logic when grouping
1012
+ siblings inside another component's child list:
1013
+
1014
+ ```python
1015
+ pn.Column(
1016
+ pn.Text("Top"),
1017
+ pn.Fragment(
1018
+ pn.Text("Middle A"),
1019
+ pn.Text("Middle B"),
1020
+ ),
1021
+ pn.Text("Bottom"),
1022
+ )
1023
+ ```
1024
+
1025
+ Args:
1026
+ *children: Child elements to expose at the parent level. ``None``
1027
+ children are dropped, which makes conditional rendering with
1028
+ ``cond and pn.Text(...)`` ergonomic.
1029
+ key: Optional key for the Fragment itself (rarely useful since
1030
+ Fragment doesn't appear in the native tree).
1031
+
1032
+ Returns:
1033
+ An [`Element`][pythonnative.Element] of type ``"__Fragment__"``.
1034
+
1035
+ Note:
1036
+ Today, returning a Fragment from a ``@pn.component`` function
1037
+ only mounts its first child as the component's root. To return
1038
+ multiple top-level elements from a function component, use a
1039
+ container such as [`Column`][pythonnative.Column] or
1040
+ [`Row`][pythonnative.Row] instead.
1041
+ """
1042
+ filtered = [c for c in children if c is not None]
1043
+ return Element("__Fragment__", {}, filtered, key=key)
1044
+
1045
+
1046
+ # ======================================================================
1047
+ # Error boundary
1048
+ # ======================================================================
773
1049
 
774
1050
 
775
1051
  def ErrorBoundary(
776
- child: Optional[Element] = None,
777
- *,
1052
+ *children: Element,
778
1053
  fallback: Optional[Any] = None,
779
1054
  key: Optional[str] = None,
780
1055
  ) -> Element:
781
- """Catch render errors in `child` and display `fallback` instead.
1056
+ """Catch render errors in the wrapped subtree and display ``fallback`` instead.
1057
+
1058
+ ``fallback`` may be an [`Element`][pythonnative.Element] or a
1059
+ callable that receives the exception and returns an ``Element``.
1060
+ Useful for isolating risky subtrees so a single failure doesn't
1061
+ crash the page.
782
1062
 
783
- `fallback` may be an [`Element`][pythonnative.Element] or a callable
784
- that receives the exception and returns an `Element`. Useful for
785
- isolating risky subtrees so a single failure doesn't crash the page.
1063
+ When multiple children are passed they're grouped under a
1064
+ [`Fragment`][pythonnative.Fragment] so the boundary still wraps a
1065
+ single logical subtree.
786
1066
 
787
1067
  Args:
788
- child: Subtree to wrap.
789
- fallback: Element to render when `child` raises during render,
790
- or a callable `fallback(err) -> Element`.
1068
+ *children: Subtree to wrap.
1069
+ fallback: Element rendered when the subtree raises during
1070
+ render, or a callable ``fallback(err) -> Element``.
791
1071
  key: Stable identity for keyed reconciliation.
792
1072
 
793
1073
  Returns:
794
- An [`Element`][pythonnative.Element] of type `"__ErrorBoundary__"`.
1074
+ An [`Element`][pythonnative.Element] of type
1075
+ ``"__ErrorBoundary__"``.
795
1076
 
796
1077
  Example:
797
1078
  ```python
@@ -806,8 +1087,16 @@ def ErrorBoundary(
806
1087
  props: Dict[str, Any] = {}
807
1088
  if fallback is not None:
808
1089
  props["__fallback__"] = fallback
809
- children = [child] if child is not None else []
810
- return Element("__ErrorBoundary__", props, children, key=key)
1090
+ if len(children) <= 1:
1091
+ kids = list(children)
1092
+ else:
1093
+ kids = [Fragment(*children)]
1094
+ return Element("__ErrorBoundary__", props, kids, key=key)
1095
+
1096
+
1097
+ # ======================================================================
1098
+ # Lists
1099
+ # ======================================================================
811
1100
 
812
1101
 
813
1102
  def FlatList(
@@ -822,17 +1111,18 @@ def FlatList(
822
1111
  style: StyleProp = None,
823
1112
  key: Optional[str] = None,
824
1113
  ) -> Element:
825
- """Virtualized scrollable list that renders items from `data` lazily.
1114
+ """Virtualized scrollable list that renders items from ``data`` lazily.
826
1115
 
827
- Backed by `UITableView` on iOS and `RecyclerView` on Android via the
828
- `VirtualList` element. Each visible row is mounted on demand by a
829
- nested [`Reconciler`][pythonnative.reconciler.Reconciler] when
1116
+ Backed by ``UITableView`` on iOS and ``RecyclerView`` on Android via
1117
+ the ``VirtualList`` element. Each visible row is mounted on demand
1118
+ by a nested
1119
+ [`Reconciler`][pythonnative.reconciler.Reconciler] when
830
1120
  ``item_height`` is specified.
831
1121
 
832
1122
  When ``item_height`` is omitted the implementation falls back to an
833
1123
  eager (non-virtualized) ``ScrollView`` of every row — keep the data
834
- set small in that mode (the fallback is convenient for short
835
- lists where virtualization overhead would dominate).
1124
+ set small in that mode (the fallback is convenient for short lists
1125
+ where virtualization overhead would dominate).
836
1126
 
837
1127
  Args:
838
1128
  data: Iterable of arbitrary item values.
@@ -852,11 +1142,12 @@ def FlatList(
852
1142
  on_item_press: Callback invoked with the row index when the
853
1143
  user taps a row (virtualized backend only).
854
1144
  style: Style dict (or list of dicts).
855
- key: Stable identity for keyed reconciliation of the list itself.
1145
+ key: Stable identity for keyed reconciliation of the list
1146
+ itself.
856
1147
 
857
1148
  Returns:
858
- An [`Element`][pythonnative.Element] of type `"VirtualList"`
859
- (virtualized) or `"ScrollView"` (eager fallback).
1149
+ An [`Element`][pythonnative.Element] of type ``"VirtualList"``
1150
+ (virtualized) or ``"ScrollView"`` (eager fallback).
860
1151
 
861
1152
  Example:
862
1153
  ```python
@@ -883,11 +1174,7 @@ def FlatList(
883
1174
  el = Element(el.type, el.props, el.children, key=key_extractor(item, i))
884
1175
  items_eager.append(el)
885
1176
  inner = Column(*items_eager, style={"spacing": separator_height} if separator_height else None)
886
- sv_props: Dict[str, Any] = {}
887
- if refresh_control is not None:
888
- sv_props["refresh_control"] = refresh_control
889
- sv_props.update(resolve_style(style))
890
- return Element("ScrollView", sv_props, [inner], key=key)
1177
+ return ScrollView(inner, refresh_control=refresh_control, style=style, key=key)
891
1178
 
892
1179
  # Virtualized path: render_item is invoked lazily by the native
893
1180
  # cell mount callback when each row scrolls into view.
@@ -928,17 +1215,16 @@ def FlatList(
928
1215
 
929
1216
  backend.add_child(content_view, native_root, "View")
930
1217
 
931
- list_props: Dict[str, Any] = {
932
- "count": len(items_list),
933
- "row_height": row_h,
934
- "mount_row": _mount_row,
935
- }
936
- if on_item_press is not None:
937
- list_props["on_row_press"] = on_item_press
938
- if refresh_control is not None:
939
- list_props["refresh_control"] = refresh_control
940
- list_props.update(resolve_style(style))
941
- return Element("VirtualList", list_props, [], key=key)
1218
+ return _make_element(
1219
+ "VirtualList",
1220
+ style=style,
1221
+ key=key,
1222
+ count=len(items_list),
1223
+ row_height=row_h,
1224
+ mount_row=_mount_row,
1225
+ on_row_press=on_item_press,
1226
+ refresh_control=refresh_control,
1227
+ )
942
1228
 
943
1229
 
944
1230
  def SectionList(
@@ -961,8 +1247,10 @@ def SectionList(
961
1247
 
962
1248
  Args:
963
1249
  sections: Each section is ``{"title": ..., "data": [...]}``.
964
- render_item: ``render_item(item, item_index, section_index) -> Element``.
965
- render_section_header: ``render_section_header(section, section_index) -> Element``.
1250
+ render_item: ``render_item(item, item_index, section_index) ->
1251
+ Element``.
1252
+ render_section_header: ``render_section_header(section,
1253
+ section_index) -> Element``.
966
1254
  item_height: Fixed row height for items, in layout units.
967
1255
  section_header_height: Fixed header height in layout units.
968
1256
  separator_height: Gap appended below each item, in layout units.
@@ -970,9 +1258,9 @@ def SectionList(
970
1258
  key: Stable identity for keyed reconciliation.
971
1259
 
972
1260
  Returns:
973
- An [`Element`][pythonnative.Element] of type `"VirtualList"`
974
- (virtualized). When ``item_height`` is omitted the layout
975
- falls back to an eager column.
1261
+ An [`Element`][pythonnative.Element] of type ``"VirtualList"``
1262
+ (virtualized). When ``item_height`` is omitted the layout falls
1263
+ back to an eager column.
976
1264
  """
977
1265
  sections_list = list(sections or [])
978
1266
 
@@ -997,9 +1285,7 @@ def SectionList(
997
1285
  else:
998
1286
  children.append(Text(str(entry["item"])))
999
1287
  inner = Column(*children, style={"spacing": separator_height} if separator_height else None)
1000
- sv_props: Dict[str, Any] = {}
1001
- sv_props.update(resolve_style(style))
1002
- return Element("ScrollView", sv_props, [inner], key=key)
1288
+ return ScrollView(inner, style=style, key=key)
1003
1289
 
1004
1290
  # Virtualized: mixed row heights aren't supported in v1, so we
1005
1291
  # use the larger of section_header_height and item_height + sep.
@@ -1032,23 +1318,24 @@ def SectionList(
1032
1318
  except Exception:
1033
1319
  pass
1034
1320
 
1035
- list_props: Dict[str, Any] = {
1036
- "count": len(flat),
1037
- "row_height": row_h,
1038
- "mount_row": _mount_row,
1039
- }
1040
- list_props.update(resolve_style(style))
1041
- return Element("VirtualList", list_props, [], key=key)
1321
+ return _make_element(
1322
+ "VirtualList",
1323
+ style=style,
1324
+ key=key,
1325
+ count=len(flat),
1326
+ row_height=row_h,
1327
+ mount_row=_mount_row,
1328
+ )
1042
1329
 
1043
1330
 
1044
1331
  # ======================================================================
1045
- # Status bar / keyboard / refresh / alert / picker
1332
+ # StatusBar / KeyboardAvoidingView / RefreshControl / Picker
1046
1333
  # ======================================================================
1047
1334
 
1048
1335
 
1049
1336
  def StatusBar(
1050
1337
  *,
1051
- style: Optional[Literal["light", "dark", "default"]] = None,
1338
+ bar_style: Optional[Literal["light", "dark", "default"]] = None,
1052
1339
  background_color: Optional[Color] = None,
1053
1340
  hidden: Optional[bool] = None,
1054
1341
  key: Optional[str] = None,
@@ -1059,8 +1346,13 @@ def StatusBar(
1059
1346
  content but applies its props to the host platform's status bar.
1060
1347
  Mount one near the top of your tree.
1061
1348
 
1349
+ The ``bar_style`` parameter is named separately from the universal
1350
+ ``style`` kwarg (which is unused here) to avoid the conflict that
1351
+ ``style="light"`` would create with the visual-style dict used
1352
+ elsewhere.
1353
+
1062
1354
  Args:
1063
- style: ``"light"`` (light icons over dark backgrounds),
1355
+ bar_style: ``"light"`` (light icons over dark backgrounds),
1064
1356
  ``"dark"`` (dark icons over light backgrounds), or
1065
1357
  ``"default"`` (system default).
1066
1358
  background_color: Color of the status-bar background (Android
@@ -1069,11 +1361,11 @@ def StatusBar(
1069
1361
  key: Stable identity for keyed reconciliation.
1070
1362
 
1071
1363
  Returns:
1072
- An [`Element`][pythonnative.Element] of type `"StatusBar"`.
1364
+ An [`Element`][pythonnative.Element] of type ``"StatusBar"``.
1073
1365
  """
1074
1366
  props: Dict[str, Any] = {}
1075
- if style is not None:
1076
- props["style"] = style
1367
+ if bar_style is not None:
1368
+ props["bar_style"] = bar_style
1077
1369
  if background_color is not None:
1078
1370
  props["background_color"] = background_color
1079
1371
  if hidden is not None:
@@ -1091,8 +1383,8 @@ def KeyboardAvoidingView(
1091
1383
 
1092
1384
  Subscribes to the platform-reported keyboard height (via
1093
1385
  [`use_keyboard_height`][pythonnative.use_keyboard_height]
1094
- internally) and applies it as bottom padding so the focused
1095
- text input stays visible.
1386
+ internally) and applies it as bottom padding so the focused text
1387
+ input stays visible.
1096
1388
 
1097
1389
  Args:
1098
1390
  *children: Children rendered inside the avoiding container.
@@ -1103,11 +1395,15 @@ def KeyboardAvoidingView(
1103
1395
 
1104
1396
  Returns:
1105
1397
  An [`Element`][pythonnative.Element] of type
1106
- `"KeyboardAvoidingView"`.
1398
+ ``"KeyboardAvoidingView"``.
1107
1399
  """
1108
- props: Dict[str, Any] = {"behavior": behavior}
1109
- props.update(resolve_style(style))
1110
- return Element("KeyboardAvoidingView", props, list(children), key=key)
1400
+ return _make_element(
1401
+ "KeyboardAvoidingView",
1402
+ *children,
1403
+ style=style,
1404
+ key=key,
1405
+ behavior=behavior,
1406
+ )
1111
1407
 
1112
1408
 
1113
1409
  def RefreshControl(
@@ -1119,16 +1415,16 @@ def RefreshControl(
1119
1415
  """Pull-to-refresh spec for [`ScrollView`][pythonnative.ScrollView] / [`FlatList`][pythonnative.FlatList].
1120
1416
 
1121
1417
  Returns a plain dict that should be passed as the
1122
- ``refresh_control=`` prop. Modeled as a dict (not an Element) so
1123
- the host scroll container can hold one without it appearing as a
1124
- child node.
1418
+ ``refresh_control=`` prop. Modeled as a dict (not an
1419
+ [`Element`][pythonnative.Element]) so the host scroll container can
1420
+ hold one without it appearing as a child node.
1125
1421
 
1126
1422
  Args:
1127
1423
  refreshing: Drive the spinner's visibility from a use_state
1128
1424
  value.
1129
- on_refresh: Callback invoked when the user pulls down past
1130
- the threshold. Set ``refreshing`` to True for the
1131
- duration of the work, then back to False on completion.
1425
+ on_refresh: Callback invoked when the user pulls down past the
1426
+ threshold. Set ``refreshing`` to ``True`` for the duration
1427
+ of the work, then back to ``False`` on completion.
1132
1428
  tint_color: Color of the spinner.
1133
1429
 
1134
1430
  Returns:
@@ -1171,53 +1467,49 @@ def Picker(
1171
1467
  on_change: Optional[Callable[[Any], None]] = None,
1172
1468
  placeholder: str = "Select…",
1173
1469
  style: StyleProp = None,
1470
+ accessibility_label: Optional[str] = None,
1471
+ accessibility_hint: Optional[str] = None,
1472
+ accessible: Optional[bool] = None,
1473
+ ref: Optional[Dict[str, Any]] = None,
1174
1474
  key: Optional[str] = None,
1175
1475
  ) -> Element:
1176
- """A select / dropdown widget.
1476
+ """A real native dropdown / select widget.
1477
+
1478
+ Renders a tappable trigger labelled with the selected item; the
1479
+ iOS handler attaches a ``UIMenu`` (system dropdown) and the Android
1480
+ handler uses a native ``Spinner``. Selecting an item fires
1481
+ ``on_change(value)``.
1177
1482
 
1178
- Implemented as a plain
1179
- [`Pressable`][pythonnative.Pressable] that, on tap, presents an
1180
- [`Alert`][pythonnative.Alert]-style action sheet listing the
1181
- options. Selecting an option fires ``on_change(value)``.
1483
+ ``items`` is an ordered list of ``{"value": Any, "label": str}``
1484
+ entries (``label`` defaults to ``str(value)`` when omitted).
1182
1485
 
1183
1486
  Args:
1184
1487
  value: Currently selected value (matched against
1185
1488
  ``items[i]["value"]``).
1186
- items: Each item is ``{"value": ..., "label": ...}``.
1187
- on_change: Callback invoked with the selected value.
1188
- placeholder: Label shown when nothing is selected.
1189
- style: Style dict applied to the trigger pressable.
1489
+ items: Selectable options.
1490
+ on_change: Callback invoked with the new value.
1491
+ placeholder: Label shown when no item matches ``value``.
1492
+ style: Style dict applied to the trigger.
1493
+ accessibility_label: Spoken description for screen readers.
1494
+ accessibility_hint: Spoken extra detail (iOS only).
1495
+ accessible: Override whether the element is exposed to AT.
1496
+ ref: Optional ``use_ref()`` dict.
1190
1497
  key: Stable identity for keyed reconciliation.
1191
1498
 
1192
1499
  Returns:
1193
- An [`Element`][pythonnative.Element] of type `"Pressable"`.
1500
+ An [`Element`][pythonnative.Element] of type ``"Picker"``.
1194
1501
  """
1195
- items_list = list(items or [])
1196
- selected_label = placeholder
1197
- for it in items_list:
1198
- if it.get("value") == value:
1199
- selected_label = str(it.get("label", value))
1200
- break
1201
-
1202
- def _open() -> None:
1203
- try:
1204
- from .alerts import Alert
1205
- except Exception:
1206
- return
1207
-
1208
- def _make_btn(item: Dict[str, Any]) -> Dict[str, Any]:
1209
- def _press() -> None:
1210
- if on_change is not None:
1211
- try:
1212
- on_change(item.get("value"))
1213
- except Exception:
1214
- pass
1215
-
1216
- return {"label": str(item.get("label", item.get("value"))), "on_press": _press}
1217
-
1218
- buttons = [_make_btn(it) for it in items_list]
1219
- buttons.append({"label": "Cancel", "style": "cancel"})
1220
- Alert.show(title=placeholder, buttons=buttons, style="action_sheet")
1221
-
1222
- label_text = Text(selected_label)
1223
- return Pressable(label_text, on_press=_open, style=style, key=key)
1502
+ return _make_element(
1503
+ "Picker",
1504
+ style=style,
1505
+ ref=ref,
1506
+ key=key,
1507
+ value=value,
1508
+ items=list(items) if items is not None else [],
1509
+ on_change=on_change,
1510
+ placeholder=placeholder,
1511
+ accessibility_label=accessibility_label,
1512
+ accessibility_hint=accessibility_hint,
1513
+ accessible=accessible,
1514
+ _defaults={"accessibility_role": "button"},
1515
+ )