Help us shape the future of Go! Take the 2026 Go Developer Survey

kyse

package module
v0.37.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 1 Imported by: 0

README

Arandu

arandu-io/kyse

The component library kyse views import to draw a screen.

Build Status Go Reference Latest Version License

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.

A form in a dialog

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.

Documentation

Overview

Package kyse holds what a caller writes in a view and a component reads, where the two need a type and neither owns it.

It is one type today. The components live under components/, and what is here is what they take rather than what they are.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CSS

type CSS string

CSS is a block of stylesheet scoped to one component instance.

components.Card(components.CardProps{
	ComponentProps: components.ComponentProps{
		Style: kyse.CSS(`
			& { gap: 6px; }
			& [data-part="title"] { letter-spacing: -0.01em; }
		`),
	},
})

Where the block goes, and why not into the page

It does not travel in the page. The policy is style-src 'self' with no unsafe-inline, so neither a style attribute nor a style element survives, and both fail by being dropped rather than by saying anything -- the markup reads correctly and the browser ignores it.

What travels is a class. `aru view:build` reads these blocks out of the source, replaces & with the class, and writes the rules into the project's stylesheet, which is served from the origin like every other asset. The class is the hash of the block's own text, so the build and the render agree without a table between them: neither knows about the other, and both are looking at the same bytes.

The one condition

The argument has to be a string literal. A block built at run time is a hash the render can compute and the build never saw, so the rule is never emitted and the element carries a class nothing styles -- which renders, and is silent. `aru view:build` refuses a call whose argument is anything else, and names the file and the line.

It is the same limit a progress bar's width is written around, for the same reason: what the stylesheet contains is decided at build time, from the text in the source.

func (CSS) Class

func (c CSS) Class() string

Class is the class this block is compiled under, or empty when there is no block.

func (CSS) Text

func (c CSS) Text() string

Text is the block as written, which is what the build reads and hashes.

Directories

Path Synopsis
Package components draws the pieces a screen is made of.
Package components draws the pieces a screen is made of.
Package fonts serves the faces a project vendored.
Package fonts serves the faces a project vendored.
Package icons draws the Phosphor icon set, one exported function per icon.
Package icons draws the Phosphor icon set, one exported function per icon.
internal
icongen command
Command icongen vendors the Phosphor icon set into Go source.
Command icongen vendors the Phosphor icon set into Go source.
Package mailui draws the messages an application sends.
Package mailui draws the messages an application sends.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL