Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ First release. Renders A2UI generative user interfaces in Jaspr, building on
- `MinimalJasprCatalog` renders the five components of the A2UI minimal catalog,
`TextComponent`, `RowComponent`, `ColumnComponent`, `ButtonComponent`, and
`TextFieldComponent`. Schemas and the catalog id come from `a2ui_core`
unchanged. `JasprComponent` is the base class for a component of your own, and
`copyWith` derives a catalog from an existing one.
unchanged. `JasprComponent` is the base class for a component of your own,
declaring its name and schema alongside `build`. `ExternalApiJasprComponent`
renders an API defined elsewhere, as the minimal components do with
`a2ui_core`'s. `copyWith` derives a catalog from an existing one.
- `ComponentScope` hands a builder its resolved properties, its children, and a
way to report errors. An action obtained through it never throws out of a
click handler. Its `instanceId` is unique to each rendered instance across
Expand Down
35 changes: 23 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,24 +266,20 @@ A backend that delivers A2UI already parsed, such as an A2A agent, goes through

### Adding a component

A component is a `JasprComponent`: an `a2ui_core` API, which owns the name and
the schema, a `build` method that turns resolved properties into HTML, and the
style rules for the classes that `build` emits. The API classes for the minimal
catalog come from `a2ui_core`. For a component of your own, define all three
together:
A component is a `JasprComponent`. It is its own `a2ui_core` API, so one class
declares the name, the schema that decides how each property binds, a `build`
method that turns resolved properties into HTML, and the style rules for the
classes that `build` emits:

```dart
class DividerApi extends ComponentApi {
class DividerComponent extends JasprComponent {
const DividerComponent();

@override
String get name => 'Divider';

@override
Schema get schema => Schema.object(properties: {});
}

class DividerComponent extends JasprComponent {
@override
final ComponentApi api = DividerApi();

@override
Component build(ComponentScope scope) => hr(classes: 'a2ui-divider');
Expand All @@ -309,14 +305,29 @@ its schema:
```dart
final catalog = MinimalJasprCatalog().copyWith(
id: 'com.example.catalog',
add: [DividerComponent()],
add: [const DividerComponent()],
);
```

`catalog.styles` now carries the divider's rule along with the minimal
components'. That is the point of deriving the bundle from the catalog rather
than writing it out: there is no second list to remember to update.

When the API is defined somewhere else, extend `ExternalApiJasprComponent` and
pass it up instead of restating its schema. The minimal catalog's components do
this with the API classes `a2ui_core` ships, so what the model is told it may
send and what the renderer draws come from the same definition:

```dart
class TextComponent extends ExternalApiJasprComponent {
TextComponent() : super(MinimalTextApi());

@override
Component build(ComponentScope scope) =>
p([Component.text(scope.string('text') ?? '')]);
}
```

`build` gets a `ComponentScope` with the properties already resolved: data
bindings read, function calls evaluated, actions turned into callbacks. Read a
value with `scope.string`, children with `scope.children()`, and the callback
Expand Down
26 changes: 13 additions & 13 deletions lib/src/catalog/basic/basic_catalog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,23 @@ class BasicJasprCatalog extends Catalog<JasprComponent> {

List<JasprComponent> _components() => [
TextComponent(),
ImageComponent(),
IconComponent(),
VideoComponent(),
AudioPlayerComponent(),
const ImageComponent(),
const IconComponent(),
const VideoComponent(),
const AudioPlayerComponent(),
RowComponent(),
ColumnComponent(),
ListComponent(),
CardComponent(),
TabsComponent(),
ModalComponent(),
DividerComponent(),
const ListComponent(),
const CardComponent(),
const TabsComponent(),
const ModalComponent(),
const DividerComponent(),
ButtonComponent(),
TextFieldComponent(),
CheckBoxComponent(),
ChoicePickerComponent(),
SliderComponent(),
DateTimeInputComponent(),
const CheckBoxComponent(),
const ChoicePickerComponent(),
const SliderComponent(),
const DateTimeInputComponent(),
];

List<FunctionImplementation> _functions({
Expand Down
17 changes: 6 additions & 11 deletions lib/src/catalog/basic/components/audio_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';
import 'package:json_schema_builder/json_schema_builder.dart';

/// `AudioPlayer`'s API from the A2UI v0.9 basic catalog.
class AudioPlayerApi extends ComponentApi {
/// Plays audio from an A2UI-provided URL using the browser's native controls.
class AudioPlayerComponent extends JasprComponent {
/// Creates an [AudioPlayerComponent].
const AudioPlayerComponent();

@override
String get name => 'AudioPlayer';

/// The schema from the A2UI v0.9 basic catalog.
@override
Schema get schema => Schema.object(
properties: {
Expand All @@ -17,15 +21,6 @@ class AudioPlayerApi extends ComponentApi {
},
required: ['url'],
);
}

/// Plays audio from an A2UI-provided URL using the browser's native controls.
class AudioPlayerComponent extends JasprComponent {
/// Creates an [AudioPlayerComponent].
AudioPlayerComponent();

@override
final ComponentApi api = AudioPlayerApi();

@override
List<StyleRule> get styles => const [
Expand Down
17 changes: 6 additions & 11 deletions lib/src/catalog/basic/components/card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,20 @@ import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';
import 'package:json_schema_builder/json_schema_builder.dart';

/// `Card`'s API from the A2UI v0.9 basic catalog.
class CardApi extends ComponentApi {
/// A card-like container around one child component.
class CardComponent extends JasprComponent {
/// Creates a [CardComponent].
const CardComponent();

@override
String get name => 'Card';

/// The schema from the A2UI v0.9 basic catalog.
@override
Schema get schema => Schema.object(
properties: {'child': CommonSchemas.componentId},
required: ['child'],
);
}

/// A card-like container around one child component.
class CardComponent extends JasprComponent {
/// Creates a [CardComponent].
CardComponent();

@override
final ComponentApi api = CardApi();

/// A transparent surface plus an outline keeps nested cards distinct
/// without tracking their depth or alternating background colours.
Expand Down
22 changes: 8 additions & 14 deletions lib/src/catalog/basic/components/check_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';
import 'package:json_schema_builder/json_schema_builder.dart';

/// `CheckBox`'s API, one of the basic catalog's components that `a2ui_core`
/// does not ship. Its schema matches the A2UI spec's basic catalog, taken
/// from Flutter's `genui` reference implementation since `a2ui_core` has none
/// to copy it from.
class CheckBoxApi extends ComponentApi {
/// A checkbox with a label, bound to the data model in both directions.
class CheckBoxComponent extends JasprComponent {
/// Creates a [CheckBoxComponent].
const CheckBoxComponent();

@override
String get name => 'CheckBox';

/// The schema from the A2UI spec's basic catalog, taken from Flutter's
/// `genui` reference implementation since `a2ui_core` has none to copy it
/// from.
@override
Schema get schema => Schema.combined(
allOf: [
Expand All @@ -25,15 +28,6 @@ class CheckBoxApi extends ComponentApi {
),
],
);
}

/// A checkbox with a label, bound to the data model in both directions.
class CheckBoxComponent extends JasprComponent {
/// Creates a [CheckBoxComponent].
CheckBoxComponent();

@override
final ComponentApi api = CheckBoxApi();

@override
List<StyleRule> get styles => const [
Expand Down
39 changes: 17 additions & 22 deletions lib/src/catalog/basic/components/choice_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,26 @@ import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';
import 'package:json_schema_builder/json_schema_builder.dart';

/// `ChoicePicker`'s API, one of the basic catalog's components that
/// `a2ui_core` does not ship. Its schema is trimmed to the fields this
/// renderer acts on: a labelled group of options, rendered as radio buttons
/// when `mutuallyExclusive` or checkboxes when `multipleSelection`.
class ChoicePickerApi extends ComponentApi {
/// A group of options the user picks one or more of, bound to the data model
/// in both directions.
///
/// `multipleSelection` (the default, matching the A2UI reference
/// implementation) renders a checkbox per option and writes back the list of
/// every option currently checked. `mutuallyExclusive` renders radio buttons
/// sharing a group name unique to this rendered instance, so only one can ever
/// be checked, and a picker with the same id on another surface, or in
/// another row of a template, stays a separate group.
class ChoicePickerComponent extends JasprComponent {
/// Creates a [ChoicePickerComponent].
const ChoicePickerComponent();

@override
String get name => 'ChoicePicker';

/// The basic catalog's schema, which `a2ui_core` does not ship, trimmed to
/// the fields this renderer acts on: a labelled group of options, rendered as
/// radio buttons when `mutuallyExclusive` or checkboxes when
/// `multipleSelection`.
@override
Schema get schema => Schema.combined(
allOf: [
Expand Down Expand Up @@ -54,23 +66,6 @@ class ChoicePickerApi extends ComponentApi {
),
],
);
}

/// A group of options the user picks one or more of, bound to the data model
/// in both directions.
///
/// `multipleSelection` (the default, matching the A2UI reference
/// implementation) renders a checkbox per option and writes back the list of
/// every option currently checked. `mutuallyExclusive` renders radio buttons
/// sharing a group name unique to this rendered instance, so only one can ever
/// be checked, and a picker with the same id on another surface, or in
/// another row of a template, stays a separate group.
class ChoicePickerComponent extends JasprComponent {
/// Creates a [ChoicePickerComponent].
ChoicePickerComponent();

@override
final ComponentApi api = ChoicePickerApi();

@override
List<StyleRule> get styles => const [
Expand Down
44 changes: 19 additions & 25 deletions lib/src/catalog/basic/components/date_time_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,28 @@ import 'package:jaspr/jaspr.dart';
import 'package:json_schema_builder/json_schema_builder.dart';
import 'package:universal_web/web.dart' as web;

/// `DateTimeInput`'s API, one of the basic catalog's components that
/// `a2ui_core` does not ship. Its schema matches the A2UI spec's basic
/// catalog, taken from Flutter's `genui` reference implementation since
/// `a2ui_core` has none to copy it from.
class DateTimeInputApi extends ComponentApi {
/// A date, time, or date-and-time input, bound to the data model in both
/// directions. Omitting `variant` renders both a date and a time picker, the
/// same default the A2UI spec's other renderers use.
///
/// Jaspr's own `onInput` converts a date, time, or datetime-local input's
/// value to a `DateTime`, which throws once the field is cleared (its
/// `valueAsNumber` is `NaN`, and `NaN.toInt()` has no web implementation).
/// This registers a raw `input` listener instead and reads the element's
/// `value` directly, which is already the exact string the schema binds, so
/// nothing needs converting or reformatting, and clearing the field writes
/// null the same way Slider and `TextField`'s number variant treat their own
/// missing-value case, rather than throwing.
class DateTimeInputComponent extends JasprComponent {
/// Creates a [DateTimeInputComponent].
const DateTimeInputComponent();

@override
String get name => 'DateTimeInput';

/// The schema from the A2UI spec's basic catalog, taken from Flutter's
/// `genui` reference implementation since `a2ui_core` has none to copy it
/// from.
@override
Schema get schema => Schema.combined(
allOf: [
Expand All @@ -29,26 +43,6 @@ class DateTimeInputApi extends ComponentApi {
),
],
);
}

/// A date, time, or date-and-time input, bound to the data model in both
/// directions. Omitting `variant` renders both a date and a time picker, the
/// same default the A2UI spec's other renderers use.
///
/// Jaspr's own `onInput` converts a date, time, or datetime-local input's
/// value to a `DateTime`, which throws once the field is cleared (its
/// `valueAsNumber` is `NaN`, and `NaN.toInt()` has no web implementation).
/// This registers a raw `input` listener instead and reads the element's
/// `value` directly, which is already the exact string the schema binds, so
/// nothing needs converting or reformatting, and clearing the field writes
/// null the same way Slider and `TextField`'s number variant treat their own
/// missing-value case, rather than throwing.
class DateTimeInputComponent extends JasprComponent {
/// Creates a [DateTimeInputComponent].
DateTimeInputComponent();

@override
final ComponentApi api = DateTimeInputApi();

@override
List<StyleRule> get styles => const [
Expand Down
18 changes: 6 additions & 12 deletions lib/src/catalog/basic/components/divider.dart
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
import 'package:a2ui_core/a2ui_core.dart';
import 'package:genui_jaspr/src/catalog/jaspr_component.dart';
import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';
import 'package:json_schema_builder/json_schema_builder.dart';

/// `Divider`'s API from the A2UI v0.9 basic catalog.
class DividerApi extends ComponentApi {
/// A dividing rule between pieces of content.
class DividerComponent extends JasprComponent {
/// Creates a [DividerComponent].
const DividerComponent();

@override
String get name => 'Divider';

/// The schema from the A2UI v0.9 basic catalog.
@override
Schema get schema => Schema.object(
properties: {
'axis': Schema.string(enumValues: ['horizontal', 'vertical']),
},
);
}

/// A dividing rule between pieces of content.
class DividerComponent extends JasprComponent {
/// Creates a [DividerComponent].
DividerComponent();

@override
final ComponentApi api = DividerApi();

@override
List<StyleRule> get styles => const [
Expand Down
Loading
Loading