1515// * `iconBefore?: ReactNode` → `Option[own Element]`; caller pre-builds the icon
1616// * Exception path on missing args → defaulted via `default_button_props`
1717//
18- // Effect annotation: every DOM mutation is `IO` here. Once @affinescript/dom
19- // publishes a typed `DOM` effect, swap the `/ IO` rows below to `/ DOM`.
18+ // Conventions: see feedback_affinescript_conventions.md — face = canonical,
19+ // effect annotation = `-{Effects}->`, DOM mutations live under the `DOM`
20+ // effect (declared in Externs.affine).
2021
21- // ── DOM FFI (inline; replace with `import @affinescript/dom` once that package
22- // ── has a stable surface and a real cross-package import story in this repo)
22+ module Button;
2323
24- extern type Element;
25- extern type Event;
26-
27- extern fn createElement(tag: ref String) -> own Element / IO;
28- extern fn createTextNode(text: ref String) -> own Element / IO;
29- extern fn setAttribute(el: mut Element, name: ref String, value: ref String) -> () / IO;
30- extern fn appendChild(parent: mut Element, child: own Element) -> () / IO;
31- extern fn addEventListener(
32- el: mut Element,
33- event: ref String,
34- handler: fn(Event) -> () / IO
35- ) -> () / IO;
24+ use Externs;
3625
3726// ── Domain types ──────────────────────────────────────────────────────────────
3827
@@ -50,13 +39,13 @@ type ButtonProps = own {
5039 class_extra: String, // appended to the computed class list
5140 button_type: String, // "button" | "submit" | "reset"
5241 aria_label: Option[String],
53- on_click: Option[fn(Event) -> () / IO ],
54- text: own Element, // pre-built label (text node or richer subtree)
55- icon_before: Option[own Element],
56- icon_after: Option[own Element],
42+ on_click: Option[fn(Externs. Event) -{Externs.DOM}- > ()],
43+ text: own Externs. Element, // pre-built label (text node or richer subtree)
44+ icon_before: Option[own Externs. Element],
45+ icon_after: Option[own Externs. Element],
5746}
5847
59- fn default_button_props(text: own Element) -> own ButtonProps {
48+ pub fn default_button_props(text: own Externs. Element) -> own ButtonProps {
6049 ButtonProps {
6150 variant: Primary,
6251 size: Md,
@@ -72,7 +61,7 @@ fn default_button_props(text: own Element) -> own ButtonProps {
7261 }
7362}
7463
75- // ── Class-name helpers (pure) ─────────────── ──────────────────────────────────
64+ // ── Class-name helpers (pure, no effect row) ──────────────────────────────────
7665
7766fn variant_class(v: ref ButtonVariant) -> String {
7867 match v {
@@ -96,28 +85,29 @@ fn loading_class(loading: Bool) -> String {
9685}
9786
9887// Joins non-empty parts with single spaces.
99- // (Stdlib presumably has String.join; this is a sketch using concat operators .)
88+ // (Stdlib presumably has String.join; this is a sketch using `++` per Q1 .)
10089fn join_classes(
10190 base: ref String,
10291 v: ref String,
10392 s: ref String,
10493 l: ref String,
10594 extra: ref String
10695) -> String {
107- let with_v = if v == "" { base } else { base ++ " " ++ v };
108- let with_s = if s == "" { with_v } else { with_v ++ " " ++ s };
109- let with_l = if l == "" { with_s } else { with_s ++ " " ++ l };
110- let with_e = if extra == "" { with_l } else { with_l ++ " " ++ extra };
96+ let with_v = if v == "" { base } else { base ++ " " ++ v };
97+ let with_s = if s == "" { with_v } else { with_v ++ " " ++ s };
98+ let with_l = if l == "" { with_s } else { with_s ++ " " ++ l };
99+ let with_e = if extra == "" { with_l } else { with_l ++ " " ++ extra };
111100 with_e
112101}
113102
114103// ── The builder ───────────────────────────────────────────────────────────────
115104//
116105// Returns an owned button element. The caller is responsible for mounting it
117- // (via @affinescript/dom's `mount`) and ultimately for letting it drop, which
118- // the runtime will detach from the live DOM.
106+ // (via @affinescript/dom's `mount` once cross-package import lands) and
107+ // ultimately for letting it drop, which the runtime will detach from the
108+ // live DOM.
119109
120- fn make_button(props: own ButtonProps) -> own Element / IO {
110+ pub fn make_button(props: own ButtonProps) -{Externs.DOM}- > own Externs. Element {
121111 let busy: Bool = props.disabled || props.loading;
122112
123113 let classes: String = join_classes(
@@ -128,65 +118,65 @@ fn make_button(props: own ButtonProps) -> own Element / IO {
128118 props.class_extra,
129119 );
130120
131- let btn = createElement ("button");
132- setAttribute (btn, "type", props.button_type);
133- setAttribute (btn, "class", classes);
121+ let btn = Externs.create_element ("button");
122+ Externs.set_attribute (btn, "type", props.button_type);
123+ Externs.set_attribute (btn, "class", classes);
134124
135125 if busy {
136- setAttribute (btn, "disabled", "");
137- setAttribute (btn, "aria-disabled", "true");
126+ Externs.set_attribute (btn, "disabled", "");
127+ Externs.set_attribute (btn, "aria-disabled", "true");
138128 }
139129
140130 if props.loading {
141- setAttribute (btn, "aria-busy", "true");
131+ Externs.set_attribute (btn, "aria-busy", "true");
142132 }
143133
144134 match props.aria_label {
145- Some(label) => setAttribute (btn, "aria-label", label)
135+ Some(label) => Externs.set_attribute (btn, "aria-label", label)
146136 None => ()
147137 }
148138
149139 match props.on_click {
150- Some(handler) => addEventListener (btn, "click", handler)
140+ Some(handler) => Externs.add_event_listener (btn, "click", handler)
151141 None => ()
152142 }
153143
154144 // Children, in order:
155145 // loading: spinner only (icons + text suppressed)
156146 // else: [icon_before?] text [icon_after?]
157147 if props.loading {
158- let spinner = createElement ("span");
159- setAttribute (spinner, "class", "a11y-button__spinner");
160- setAttribute (spinner, "aria-hidden", "true");
161- appendChild (btn, spinner);
148+ let spinner = Externs.create_element ("span");
149+ Externs.set_attribute (spinner, "class", "a11y-button__spinner");
150+ Externs.set_attribute (spinner, "aria-hidden", "true");
151+ Externs.append_child (btn, spinner);
162152 } else {
163153 match props.icon_before {
164154 Some(icon) => {
165- let wrap = createElement ("span");
166- setAttribute (wrap, "class", "a11y-button__icon-before");
167- setAttribute (wrap, "aria-hidden", "true");
168- appendChild (wrap, icon);
169- appendChild (btn, wrap);
155+ let wrap = Externs.create_element ("span");
156+ Externs.set_attribute (wrap, "class", "a11y-button__icon-before");
157+ Externs.set_attribute (wrap, "aria-hidden", "true");
158+ Externs.append_child (wrap, icon);
159+ Externs.append_child (btn, wrap);
170160 }
171161 None => ()
172162 }
173163 }
174164
175165 // Text wrapper is always present (loading just hides icons, not the label
176166 // — preserves layout stability per WCAG 2.1.4.13 advisory).
177- let text_wrap = createElement ("span");
178- setAttribute (text_wrap, "class", "a11y-button__text");
179- appendChild (text_wrap, props.text);
180- appendChild (btn, text_wrap);
167+ let text_wrap = Externs.create_element ("span");
168+ Externs.set_attribute (text_wrap, "class", "a11y-button__text");
169+ Externs.append_child (text_wrap, props.text);
170+ Externs.append_child (btn, text_wrap);
181171
182172 if !props.loading {
183173 match props.icon_after {
184174 Some(icon) => {
185- let wrap = createElement ("span");
186- setAttribute (wrap, "class", "a11y-button__icon-after");
187- setAttribute (wrap, "aria-hidden", "true");
188- appendChild (wrap, icon);
189- appendChild (btn, wrap);
175+ let wrap = Externs.create_element ("span");
176+ Externs.set_attribute (wrap, "class", "a11y-button__icon-after");
177+ Externs.set_attribute (wrap, "aria-hidden", "true");
178+ Externs.append_child (wrap, icon);
179+ Externs.append_child (btn, wrap);
190180 }
191181 None => ()
192182 }
0 commit comments