Rask
Rask
v0.23.1-alpha.0.494

Components

Composer

A message box with buttons around it, for a chat or an AI prompt. It sends the form it is in from the keyboard, and what was typed travels with that submit.

Drawn and behaving as Flux UI's composer does, measured against its live docs.

Composer

Type a few lines and press Ctrl+Enter or Cmd+Enter: the form is sent, and the box grows a line at a time until then.

ComposerExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ComposerExample : Component
{
    private readonly Chat _chat = new();
    private string? _sent;

    protected override Component? Render() =>
        Div.Class("space-y-3")[
            Form.Model(_chat).OnSubmit(_ => Send())[
                Ui.Composer.Bind(() => _chat.Prompt).Label("Prompt").LabelSrOnly().Placeholder("How can I help you today?").Id("ui-composer-prompt")
                    .ActionsLeading([
                        Ui.Button.Sm.Subtle.Icon(Ui.IconName.PaperClip),
                        Ui.Button.Sm.Subtle.Icon(Ui.IconName.Slash),
                        Ui.Button.Sm.Subtle.Icon(Ui.IconName.AdjustmentsHorizontal),
                    ])
                    .ActionsTrailing([
                        Ui.Button.Sm.Filled.Icon(Ui.IconName.Microphone),
                        Ui.Button.Type(Ui.ButtonType.Submit).Sm.Primary.Icon(Ui.IconName.PaperAirplane),
                    ])
            ],
            Ui.Text.Data("testid", "ui-composer-sent")[_sent is null ? "Nothing sent yet." : "Sent: " + _sent]
        ];

    private void Send()
    {
        if (_chat.Prompt.Trim().Length > 0)
        {
            _sent = _chat.Prompt;
            _chat.Prompt = "";
        }
    }

    public sealed class Chat
    {
        public string Prompt { get; set; } = "";
    }
}
Live result

Nothing sent yet.

With header

A file about to be sent, above the box.

ComposerHeaderExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ComposerHeaderExample : Component
{
    private bool _attached = true;

    protected override Component? Render() =>
        Ui.Composer.Of<string>().Label("Prompt").LabelSrOnly().Placeholder("How can I help you today?")
            .Header(_attached
                ? Div.Class("relative overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700")[
                    Img.Src("/img/avatars/amber.svg").Alt("Profile picture").Class("size-14"),
                    Div.Class("absolute top-0 right-0 p-1")[
                        Ui.Button.Xs.Filled.Icon(Ui.IconName.XMark).Class("rounded-full").Tooltip("Remove picture")
                            .OnClick(() => { _attached = false; })
                    ]
                ]
                : null)
            .ActionsLeading([
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.PaperClip),
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.Slash),
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.AdjustmentsHorizontal),
            ])
            .ActionsTrailing([
                Ui.Button.Sm.Filled.Icon(Ui.IconName.Microphone),
                Ui.Button.Type(Ui.ButtonType.Submit).Sm.Primary.Icon(Ui.IconName.PaperAirplane),
            ]);
}
Live result
Profile picture

Inline

The buttons in one row with the box, the leading ones before it.

ComposerInlineExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ComposerInlineExample : Component
{
    protected override Component? Render() =>
        Ui.Composer.Of<string>().Rows(1).Inline().Label("Prompt").LabelSrOnly().Placeholder("How can I help you today?")
            .ActionsLeading(Ui.Button.Sm.Ghost.Icon(Ui.IconName.Plus))
            .ActionsTrailing([
                Ui.Button.Sm.Filled.Icon(Ui.IconName.Microphone),
                Ui.Button.Type(Ui.ButtonType.Submit).Sm.Primary.Icon(Ui.IconName.PaperAirplane),
            ]);
}
Live result

Input variant

The corners of the other inputs.

ComposerInputVariantExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ComposerInputVariantExample : Component
{
    protected override Component? Render() =>
        Ui.Composer.Of<string>().Variant(Ui.ComposerVariant.Input).Label("Message").Placeholder("What's on your mind?")
            .ActionsLeading([
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.PaperClip),
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.Slash),
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.AdjustmentsHorizontal),
            ])
            .ActionsTrailing(Ui.Button.Sm.Filled.Icon(Ui.IconName.Microphone));
}
Live result

Height

Four lines to start with, eight before it scrolls.

ComposerHeightExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ComposerHeightExample : Component
{
    protected override Component? Render() =>
        Ui.Composer.Of<string>().Rows(4).MaxRows(8).Label("Prompt").LabelSrOnly().Placeholder("How can I help you today?")
            .ActionsLeading([
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.PaperClip),
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.Slash),
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.AdjustmentsHorizontal),
            ])
            .ActionsTrailing([
                Ui.Button.Sm.Filled.Icon(Ui.IconName.Microphone),
                Ui.Button.Type(Ui.ButtonType.Submit).Sm.Primary.Icon(Ui.IconName.PaperAirplane),
            ]);
}
Live result

Submit behavior

Enter alone sends the form, and Shift+Enter is a new line.

ComposerSubmitExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ComposerSubmitExample : Component
{
    private readonly Chat _chat = new();
    private string? _sent;

    protected override Component? Render() =>
        Div.Class("space-y-3")[
            Form.Model(_chat).OnSubmit(_ => Send())[
                Ui.Composer.Bind(() => _chat.Prompt).Enter.Label("Prompt").LabelSrOnly().Placeholder("How can I help you today?")
                    .Id("ui-composer-enter")
                    .ActionsLeading([
                        Ui.Button.Sm.Subtle.Icon(Ui.IconName.PaperClip),
                        Ui.Button.Sm.Subtle.Icon(Ui.IconName.Slash),
                        Ui.Button.Sm.Subtle.Icon(Ui.IconName.AdjustmentsHorizontal),
                    ])
                    .ActionsTrailing([
                        Ui.Button.Sm.Filled.Icon(Ui.IconName.Microphone),
                        Ui.Button.Type(Ui.ButtonType.Submit).Sm.Primary.Icon(Ui.IconName.PaperAirplane),
                    ])
            ],
            Ui.Text.Data("testid", "ui-composer-enter-sent")[_sent is null ? "Nothing sent yet." : "Sent: " + _sent]
        ];

    private void Send()
    {
        if (_chat.Prompt.Trim().Length > 0)
        {
            _sent = _chat.Prompt;
            _chat.Prompt = "";
        }
    }

    public sealed class Chat
    {
        public string Prompt { get; set; } = "";
    }
}
Live result

Nothing sent yet.

Rich text

An editor in the Input slot stands for the box, drawn without a box of its own and bound to the same value.

ComposerRichTextExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ComposerRichTextExample : Component
{
    protected override Component? Render() =>
        Ui.Composer.Of<string>().Rows(3).Label("Prompt").LabelSrOnly().Placeholder("How can I help you today?")
            .Input(Ui.Editor.Toolbar("bold italic bullet ordered | link | align"))
            .ActionsLeading([
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.PaperClip),
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.Slash),
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.AdjustmentsHorizontal),
            ])
            .ActionsTrailing([
                Ui.Button.Sm.Filled.Icon(Ui.IconName.Microphone),
                Ui.Button.Type(Ui.ButtonType.Submit).Sm.Primary.Icon(Ui.IconName.PaperAirplane),
            ]);
}
Live result


Disabled

The box and every button in it are locked.

ComposerDisabledExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ComposerDisabledExample : Component
{
    protected override Component? Render() =>
        Ui.Composer.Of<string>().Disabled().Label("Prompt").LabelSrOnly().Placeholder("How can I help you today?")
            .ActionsLeading([
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.PaperClip),
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.Slash),
                Ui.Button.Sm.Subtle.Icon(Ui.IconName.AdjustmentsHorizontal),
            ])
            .ActionsTrailing([
                Ui.Button.Sm.Filled.Icon(Ui.IconName.Microphone),
                Ui.Button.Type(Ui.ButtonType.Submit).Sm.Primary.Icon(Ui.IconName.PaperAirplane),
            ]);
}
Live result

Invalid

Send it empty: the form holds a message for the prompt, and the composer is drawn invalid.

ComposerInvalidExample.cs
using System.ComponentModel.DataAnnotations;

namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ComposerInvalidExample : Component
{
    private readonly Chat _chat = new();

    // Send it empty: the form holds a message for the prompt, and the composer is drawn invalid until it is fixed.
    protected override Component? Render() =>
        Form.Model(_chat).OnSubmit(_ => { _chat.Prompt = ""; })[
            Ui.Composer.Bind(() => _chat.Prompt).Label("Prompt").LabelSrOnly().Placeholder("How can I help you today?").Id("ui-composer-invalid")
                .ActionsLeading([
                    Ui.Button.Sm.Subtle.Icon(Ui.IconName.PaperClip),
                    Ui.Button.Sm.Subtle.Icon(Ui.IconName.Slash),
                    Ui.Button.Sm.Subtle.Icon(Ui.IconName.AdjustmentsHorizontal),
                ])
                .ActionsTrailing([
                    Ui.Button.Sm.Filled.Icon(Ui.IconName.Microphone),
                    Ui.Button.Type(Ui.ButtonType.Submit).Sm.Primary.Icon(Ui.IconName.PaperAirplane),
                ])
        ];

    public sealed class Chat
    {
        [Required(ErrorMessage = "Write a prompt first.")]
        public string Prompt { get; set; } = "";
    }
}
Live result

Behaviour

The keys that send

Ctrl+Enter or Cmd+Enter sends the form (Flux's cmd+enter); .Enter makes Enter alone send it and leaves Shift+Enter for a new line. The keys are the runtime's (data-rask-submit-keys), in the keydown itself: no newline lands first, and the form is submitted as its submit button would submit it. Outside a form the keys do nothing, and neither key empties the box, as on Flux's page. Typing sends nothing.

It grows with its text

Two lines unless Rows says otherwise; the box grows a line at a time with its text up to MaxRows, ten unless set, and scrolls from there (data-rask-grow). A box the page empties keeps its height, as Flux's does.

Slots and states

Header, above the box, Footer, ActionsLeading and ActionsTrailing take any component; Input takes a Ui.Editor, which is then as tall as Rows and MaxRows say and bound to the composer's value.

Disabled() locks the box and every Ui.Button in its slots. Invalid(), or a bound member the form calls invalid, draws the red border. Label and Description wrap it in a field, and LabelSrOnly() and DescriptionSrOnly() keep them for screen readers only; the composer is a role="group" named by its label.

Reference

Ui.Composer

flux:composer

A message box with buttons around it, which sends the form it is in from the keyboard.

Step
Takes
What it does
.Bind(…)() => a TThe member it reads and writes: () => m.Prompt. Or Value with OnChange.
.Value(…)T, the type it editsThe text, for a page that holds it itself.
.OnChange(…)a handler given the new valueRuns with the text when it is committed.
.AfterBind(…)a handler given the new valueRuns after Bind has written the model.
.Validate(…)a validatorA rule the text is checked by when it is written.
.Live()boolWrites the model as it is typed, after a 150 ms pause. Without it the text travels with the submit.
.Debounce(…)a TimeSpan: 300.MillisecondsWrites the model once typing has paused this long.
.Submit(…)
Ui.ComposerSubmit
CmdEnter
Enter

Each is a step of its own too, unless an HTML tag has its name.

Which key sends the form: Ctrl/Cmd + Enter when unset, or Enter (.Enter).
.Rows(…)intHow many lines the box shows to start with. Two when unset.
.MaxRows(…)intHow many lines it grows to before it scrolls. Ten when unset.
.Inline()boolThe buttons in one row with the box, the leading ones before it.
.Variant(…)
Ui.ComposerVariant
Default
Input

Each is a step of its own too, unless an HTML tag has its name.

The corners: a message box's when unset, or those of the other inputs.
.Placeholder(…)stringShown while the box is empty.
.Label(…)stringDraws a field around it, with this label and the error.
.LabelSrOnly()boolKeeps the label for screen readers only.
.Description(…)stringHelp text in the field around it.
.DescriptionSrOnly()boolKeeps the description for screen readers only.
.Input(…)ComponentWhat stands where the box is: a Ui.Editor for rich text.
.Header(…)ComponentWhat sits above the box: the files about to be sent.
.Footer(…)ComponentWhat sits under the buttons.
.ActionsLeading(…)ComponentThe buttons at the start of the row under the box.
.ActionsTrailing(…)ComponentThe buttons at its end, the submit button among them.
.Name(…)stringWhat the text is posted under from a plain form.
.Disabled()boolLocks the box and every button in it.
.Invalid()boolError styling the form did not ask for.
.ShowValidation()boolFalse leaves the member's message to a Ui.Error placed elsewhere.
Confirm