arandu-io/kyse
The component library kyse views import to draw a screen.
What this is
kyse names two things, and the name is overloaded on purpose. One is the
template language an Arandu view is written in: a .kyse.go file, compiled to
plain Go by the compiler in aru/internal/kyse — a build step, run by
aru view:build, never a runtime a request passes through. The other is this
repository: the library of components, written in that language, that a view
imports to draw a screen.
The components themselves are adapted from shadcn/ui by way of shadcn-htmx, and
importing the second is how a view writes the first.
Install
go get github.com/arandu-io/kyse/components
That is the whole installation. Nothing is copied into your project, there is no
directory to keep in sync, and aru view:build has nothing extra to compile.
Use
//go:build kyse
package posts
import "github.com/arandu-io/kyse/components"
@extends('layouts.app')
@section('content')
<form method="post" action="{{ .SubmitURL }}">
@csrf
{!! components.Field(components.FieldProps{
Name: "title",
Label: "Title",
Value: .Post.Title,
Page: .,
Required: true,
}) !!}
{!! components.Button(components.ButtonProps{
Label: "Publish",
Type: "submit",
}) !!}
</form>
@endsection
Every component is an ordinary exported Go function that returns
template.HTML. That is the point of it: a component that does not exist is
undefined: components.Buton and a prop that does not exist is unknown field Labl, both at the line of the .kyse.go you wrote. A component library
resolved by string at run time would report the same two mistakes as a blank
space and a 500.
What is here
| group |
components |
| Actions |
Button ButtonGroup SplitButton CopyButton Link DeleteRow OptimisticToggle DialogTrigger |
| Forms |
Field Input Label Textarea Checkbox RadioGroup Switch Select Combobox Autocomplete InputGroup RangeSlider NumberInput Password Masked OneTimeCode DateTimePicker Calendar ColorPicker FileUpload SegmentedControl Rating EditInPlace Output ActiveSearch |
| Layout |
Card ContainerCard Item Separator Sidebar Toolbar Menubar Figure |
| Display |
Avatar Badge Kbd Skeleton Table DataTable StatCard Status Meter Highlight RelativeTime ResponsiveImage MediaPlayer Carousel Feed Tree |
| Feedback |
Alert Toast Progress Empty Dialog FormDialog |
| Overlays |
Drawer Popover HoverCard Tooltip DropdownMenu Command Collapsible |
| Navigation |
Accordion Breadcrumb Tabs Pagination SkipLink ThemeToggle LoadMore LazyLoad |
Seventy-eight, one per components/*.kyse.go. What is here is what has been
adapted, not what is planned.
An alert dialog is not a component of its own: it is the Alert field on
Dialog, because a component whose whole difference is derived from one
decision is that decision. An autosizing textarea is the same, the Autosize
field on Textarea.
Two more packages travel in the same module and neither draws a page. mailui
draws the messages an application sends, in tables and inline attributes,
because a stylesheet does not survive a mail client. fonts serves the faces a
project vendored and writes the preload links for them.
FormDialog is a create or edit form drawn over the list it belongs to -- "New
discount" over the discounts -- and DialogTrigger opens it. The trigger is a
link to the page that holds the same form, so a browser that never runs the
script follows it and the form works as a page:
{!! components.DialogTrigger(components.DialogTriggerProps{
Dialog: "new-discount",
Label: "New discount",
Href: "/discounts/new",
}) !!}
{!! components.FormDialog(components.FormDialogProps{
ID: "new-discount",
Title: "New discount",
Description: "Customers apply it at checkout.",
Action: "/discounts",
Token: .Page.CSRFToken(),
Page: .Page,
SubmitLabel: "Create discount",
Body: []template.HTML{
components.Field(components.FieldProps{Name: "name", Label: "Name", Page: .Page}),
components.Field(components.FieldProps{Name: "amount", Label: "Amount", Type: "number", Page: .Page}),
},
}) !!}
The form posts to the same action as the page does, with no fragment and no
script. When the handler refuses it, the framework sends the person back to
where they posted from -- the list -- with the messages and what they typed.
The list is drawn again, each field finds its own message through Page, and
the dialog opens itself: it posts its ID in the hidden _dialog field, that
value comes back with the rest of the typed input, and the dialog whose ID it
names is drawn open with the form-dialog behaviour, which Hesape's script
turns into a modal with the cursor on the first refused field. A handler that
sends the person back with a sentence instead of with what they typed sets
Open. The behaviour needs Hesape v0.66.0 or later; without it the dialog is
shown in place, not as a modal.
Esc, Cancel and the close button close it, and focus returns to the trigger.
Enter in a field submits the form, because Cancel and the close button belong to
a second, empty method="dialog" form. Size is sm, md or lg from the
small breakpoint up; below it the dialog takes the whole screen. A confirmation
with something to fill in -- "Cancel subscription" with a reason -- is the same
component with Alert: true and SubmitVariant: "destructive"; one with
nothing to fill in is Dialog.
Icons
Phosphor (MIT), duotone weight, as
1512 exported functions in github.com/arandu-io/kyse/icons:
{!! icons.Trash(icons.Props{Label: "Delete this post"}) !!}
One function per icon, rather than go:embed or a map keyed by name, because
the linker can prove a function is unreachable and cannot prove that about a map
lookup. An application carries the icons it calls instead of retaining all 1512
through a runtime registry. See ADR 0033.
No Label means the icon is decorative and is marked aria-hidden; a Label
makes it the accessible name, which is what an icon-only button needs. There is
no size and no colour prop: the svg is currentColor at 1em, and both lose to
the stylesheet rule of whatever contains it.
The paths are regenerated from a pinned commit with go generate ./..., and
Phosphor's licence sits in icons/LICENSE.md beside them.
The stylesheet
The markup carries semantic class names — btn, field, card — and the rules
behind them ship with the skeleton, under resources/css/basecoat/. A project
created with aru new already has them.
That split is deliberate. The same button written with utility classes is 443
characters of markup; written as class="btn" it is thirty. The rules exist once
in a stylesheet you own and can edit, rather than once per element in every view
that draws a button.
Working on a component
The sources are the .kyse.go files under components/, and the .go beside
each one is compiled output that is committed — a module whose generated files
are missing is a module go get cannot use.
aru view:build
Run it from the root of this module. aru view:build compiles the views of
whatever module it is in: a project keeps them under resources/views, a library
keeps them at its root, and the command looks rather than being told. Commit what
it writes.
Where this came from
The component set, the data-variant / data-size attribute API and the HTMX
patterns are adapted from shadcn-htmx
(MIT). The stylesheet is Basecoat (MIT).
The icons are Phosphor (MIT). Every
licence travels with what was taken, and THIRD_PARTY.md says
what came from where: Basecoat's notice sits beside the vendored CSS in the
skeleton, Phosphor's in icons/LICENSE.md, and shadcn-htmx's
in THIRD_PARTY.md, because what was taken from it is a shape rather than a file
and there is no directory to put it next to. This project's own is in
LICENSE.md.
Nothing arrives through npm and nothing is fetched from a CDN. There is no
package.json in an Arandu project and the Content-Security-Policy is
script-src 'self', so a script from another host would not run even if one were
referenced.
Learning Arandu
The API reference is generated from the doc comments and lives on
pkg.go.dev. Every exported
symbol carries one, and that is deliberate: it is the documentation that cannot
drift from the code, because it sits in the same file.
The CLI documents itself. aru help lists every command, and each one explains
what it writes and what to do with it. aru doctor explains what it found and
what breaks, not which rule was violated.
The guide is published at arandu.io/docs, and the
site is itself an Arandu application. Where the guide and a doc comment
disagree, the doc comment sits next to the code and is the one to trust.
Contributing
See CONTRIBUTING.md.
Security Vulnerabilities
Please review our security policy on how to report a
vulnerability. Never open a public issue for one.
License
Open-sourced software licensed under the MIT license.