akanjs 3.1.0-rc.7 → 3.1.0-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/href.ts CHANGED
@@ -1,13 +1,23 @@
1
1
  export class Href {
2
2
  static readonly #blocked = new Set(["javascript:", "vbscript:", "data:"]);
3
3
 
4
+ static #parse(href: string, base?: string): URL | null {
5
+ try {
6
+ return new URL(href, base);
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
11
+
4
12
  /** External is what the browser takes off-site: any absolute URL with a scheme, and `//`/`\\` network-path
5
- * forms it resolves to another host. javascript:, vbscript: and data: never count. */
13
+ * forms that name a host. javascript:, vbscript: and data: never count. */
6
14
  static isExternal(href: string): boolean {
7
15
 
8
- if (URL.canParse(href)) return !Href.#blocked.has(new URL(href).protocol);
16
+ const absolute = Href.#parse(href);
17
+ if (absolute) return !Href.#blocked.has(absolute.protocol);
9
18
 
10
- return /^[\\/]{2}/.test(href.replace(/[\t\n\r]/g, "").replace(/^[\u0000-\u0020]+/, ""));
19
+ const value = href.replace(/[\t\n\r]/g, "").replace(/^[\u0000-\u0020]+/, "");
20
+ return /^[\\/]{2}/.test(value) && !!Href.#parse(value, "https://localhost")?.hostname;
11
21
  }
12
22
 
13
23
  /** An explicit http(s) scheme in any case; `//host` and `httpbin/x` do not qualify. */
@@ -15,10 +25,11 @@ export class Href {
15
25
  return /^https?:/i.test(href);
16
26
  }
17
27
 
18
- /** Canonical absolute URL for an external href: scheme-relative and backslash hosts resolve against the page's
19
- * http(s) origin, or https: when the page is a native `app://` shell. */
28
+ /** Canonical absolute URL for an external href: follows the browser on an http(s) document base; on a native
29
+ * `app://` base an absolute URL resolves as written and a network path resolves against https:. */
20
30
  static toExternalUrl(href: string, base: string): string {
21
- const baseProtocol = URL.canParse(base) ? new URL(base).protocol : "";
22
- return new URL(href, /^https?:$/.test(baseProtocol) ? base : "https://localhost").href;
31
+ const baseUrl = Href.#parse(base);
32
+ if (baseUrl && /^https?:$/.test(baseUrl.protocol)) return Href.#parse(href, baseUrl.href)?.href ?? href;
33
+ return (Href.#parse(href) ?? Href.#parse(href, "https://localhost"))?.href ?? href;
23
34
  }
24
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.1.0-rc.7",
3
+ "version": "3.1.0-rc.9",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -1,11 +1,11 @@
1
1
  export declare class Href {
2
2
  #private;
3
3
  /** External is what the browser takes off-site: any absolute URL with a scheme, and `//`/`\\` network-path
4
- * forms it resolves to another host. javascript:, vbscript: and data: never count. */
4
+ * forms that name a host. javascript:, vbscript: and data: never count. */
5
5
  static isExternal(href: string): boolean;
6
6
  /** An explicit http(s) scheme in any case; `//host` and `httpbin/x` do not qualify. */
7
7
  static hasWebScheme(href: string): boolean;
8
- /** Canonical absolute URL for an external href: scheme-relative and backslash hosts resolve against the page's
9
- * http(s) origin, or https: when the page is a native `app://` shell. */
8
+ /** Canonical absolute URL for an external href: follows the browser on an http(s) document base; on a native
9
+ * `app://` base an absolute URL resolves as written and a network path resolves against https:. */
10
10
  static toExternalUrl(href: string, base: string): string;
11
11
  }
package/ui/Clipboard.tsx CHANGED
@@ -26,6 +26,7 @@ export const Clipboard = ({ text, className }: ClipboardProps): ReactElement =>
26
26
  };
27
27
  return (
28
28
  <button
29
+ type="button"
29
30
  onClick={async () => {
30
31
  await handleCopy();
31
32
  }}
package/ui/Data/Item.tsx CHANGED
@@ -194,6 +194,7 @@ export const Action = <T extends string, M extends { id: string }, L extends { i
194
194
  };
195
195
  return action === "edit" ? (
196
196
  <button
197
+ type="button"
197
198
  className={buttonRecipe({ variant: outline ? "outline" : "ghost", size: "icon" }, [
198
199
  "m-1 size-8 text-center",
199
200
  outline && "border-dashed",
@@ -204,6 +205,7 @@ export const Action = <T extends string, M extends { id: string }, L extends { i
204
205
  </button>
205
206
  ) : action === "view" ? (
206
207
  <button
208
+ type="button"
207
209
  className={buttonRecipe({ variant: outline ? "outline" : "ghost", size: "icon" }, [
208
210
  "m-1 size-8 text-center",
209
211
  outline && "border-dashed",
@@ -219,6 +221,7 @@ export const Action = <T extends string, M extends { id: string }, L extends { i
219
221
  onConfirm={() => void storeDo[namesOfSlice.removeModel](model.id)}
220
222
  >
221
223
  <button
224
+ type="button"
222
225
  className={buttonRecipe({ variant: outline ? "outline" : "ghost", size: "icon" }, [
223
226
  "m-1 size-8 text-center",
224
227
  outline && "border-dashed",
@@ -183,6 +183,7 @@ const Item = ({
183
183
  {children}
184
184
  {removable && (
185
185
  <button
186
+ type="button"
186
187
  className={recipe({ variant: "outline", size: "xs" }, [
187
188
  "size-6 border-destructive p-0 text-destructive hover:bg-destructive hover:text-destructive-foreground",
188
189
  removeClassName,
@@ -50,6 +50,7 @@ export const List = <Item,>({
50
50
  {renderItem(item, idx)}
51
51
  <div className="flex gap-2 border-border border-l pl-2">
52
52
  <button
53
+ type="button"
53
54
  className={recipe(
54
55
  { variant: "outline", size: "icon" },
55
56
  "size-6 border-destructive p-0 text-destructive hover:bg-destructive hover:text-destructive-foreground",
@@ -66,6 +67,7 @@ export const List = <Item,>({
66
67
  </Fragment>
67
68
  ))}
68
69
  <button
70
+ type="button"
69
71
  className={recipe({ variant: "outline" })}
70
72
  onClick={() => {
71
73
  onAdd();
@@ -151,6 +153,7 @@ export const TextList = ({
151
153
  disabled={disabled}
152
154
  />
153
155
  <button
156
+ type="button"
154
157
  className={recipe(
155
158
  { variant: "outline", size: "icon" },
156
159
  "size-6 border-destructive p-0 text-destructive hover:bg-destructive hover:text-destructive-foreground",
@@ -169,6 +172,7 @@ export const TextList = ({
169
172
  <div className="my-5 h-[0.5px] bg-foreground/20" />
170
173
  {texts.length < maxlength ? (
171
174
  <button
175
+ type="button"
172
176
  className={recipe({ variant: "outline" }, "w-full")}
173
177
  onClick={() => {
174
178
  onChange([...texts, ""]);
package/ui/Input.tsx CHANGED
@@ -272,6 +272,7 @@ const DefaultPassword = ({
272
272
  />
273
273
  <div className="absolute top-1/2 right-2 flex -translate-y-1/2 items-center justify-center">
274
274
  <button
275
+ type="button"
275
276
  onClick={() => {
276
277
  setShowPassword(!showPassword);
277
278
  }}
@@ -127,7 +127,7 @@ export const LeftSider = ({ className, children, open, width, close, onCancel }:
127
127
  }}
128
128
  >
129
129
  {close ?? (
130
- <button className={buttonRecipe({ variant: "ghost", size: "icon" })}>
130
+ <button type="button" className={buttonRecipe({ variant: "ghost", size: "icon" })}>
131
131
  <BiX />
132
132
  </button>
133
133
  )}
@@ -53,7 +53,7 @@ export const CsrLink = ({
53
53
  const isExternal = Href.isExternal(href);
54
54
  const url = href.startsWith("#") ? `${csrLocation.pathname}#${hash}` : href;
55
55
  if (isExternal) {
56
- const target = Href.toExternalUrl(href, window.location.href);
56
+ const target = Href.toExternalUrl(href, document.baseURI);
57
57
  void openExternalUrl(target).catch(() => window.open(target, "_blank", "noopener,noreferrer"));
58
58
  } else if (replace) router.replace(url, { scrollToTop });
59
59
  else router.push(url, { scrollToTop });
package/ui/Model/Edit.tsx CHANGED
@@ -24,7 +24,7 @@ export default function Edit({ trigger, children, slice, modelId, modal, renderT
24
24
  <>
25
25
  <EditWrapper className="contents" slice={slice} modelId={modelId} modal={modal} draft={draft}>
26
26
  {trigger ?? (
27
- <button className={buttonRecipe({ variant: "primary" })}>
27
+ <button type="button" className={buttonRecipe({ variant: "primary" })}>
28
28
  <AiOutlineEdit /> {l("base.edit")}
29
29
  </button>
30
30
  )}
package/ui/Model/New.tsx CHANGED
@@ -45,7 +45,7 @@ export default function New({
45
45
  draft={draft}
46
46
  >
47
47
  {trigger ?? (
48
- <button className={buttonRecipe({ variant: "primary" })}>
48
+ <button type="button" className={buttonRecipe({ variant: "primary" })}>
49
49
  <AiOutlinePlus /> {l("base.new")}
50
50
  </button>
51
51
  )}
@@ -89,6 +89,7 @@ export default function SureToRemove({
89
89
  bodyClassName="border-destructive"
90
90
  action={
91
91
  <button
92
+ type="button"
92
93
  className={buttonRecipe({ variant: "destructive" }, "w-full")}
93
94
  disabled={typeNameToRemove && repeatName !== name}
94
95
  onClick={async () => {
@@ -45,7 +45,12 @@ const EditAction = ({ modelName, label, onAct, closeView }: ActionProps) => {
45
45
  .desc(`Turn the open ${modelName} from its detail view into the edit form.`)
46
46
  .exec(onAct);
47
47
  return (
48
- <button className={buttonRecipe({ variant: "primary" }, "w-full")} onClick={editModel} {...agentAttrs(editModel)}>
48
+ <button
49
+ type="button"
50
+ className={buttonRecipe({ variant: "primary" }, "w-full")}
51
+ onClick={editModel}
52
+ {...agentAttrs(editModel)}
53
+ >
49
54
  <AiOutlineEdit /> {label}
50
55
  </button>
51
56
  );
@@ -59,6 +64,7 @@ const SaveAction = ({ modelName, label, onAct, closeView }: ActionProps) => {
59
64
  .exec(onAct);
60
65
  return (
61
66
  <button
67
+ type="button"
62
68
  className={buttonRecipe({ variant: "primary" }, "w-full")}
63
69
  onClick={submitModel}
64
70
  {...agentAttrs(submitModel)}
package/ui/ObjectId.tsx CHANGED
@@ -26,7 +26,11 @@ export const ObjectId = ({ id }: ObjectIdProps) => {
26
26
  <Tooltip content={id} variant="primary">
27
27
  <div className="font-semibold text-xs">{shortenedId}</div>
28
28
  </Tooltip>
29
- <button className={recipe({ variant: "ghost", size: "icon" }, "size-6 rounded-full")} onClick={handleCopyClick}>
29
+ <button
30
+ type="button"
31
+ className={recipe({ variant: "ghost", size: "icon" }, "size-6 rounded-full")}
32
+ onClick={handleCopyClick}
33
+ >
30
34
  {isCopied ? <AiOutlineCheck /> : <AiOutlineCopy />}
31
35
  </button>
32
36
  </div>
package/ui/Signal/Doc.tsx CHANGED
@@ -74,7 +74,10 @@ export const DocSetting = ({ fetch = runtimeFetch, search, onSearch }: DocSettin
74
74
  <Toolbar>
75
75
  <ToolbarField label={l.trans(signalText.baseUrl)}>
76
76
  <Copy text={fetch.origin}>
77
- <button className={buttonRecipe({ variant: "ghost", size: "sm" }, "font-mono text-foreground/80")}>
77
+ <button
78
+ type="button"
79
+ className={buttonRecipe({ variant: "ghost", size: "sm" }, "font-mono text-foreground/80")}
80
+ >
78
81
  {fetch.origin}
79
82
  <AiOutlineCopy className="text-foreground/40" />
80
83
  </button>
@@ -173,6 +176,7 @@ export const DocAuthModal = ({ children }: DocAuthModalProps) => {
173
176
  title={l.trans(signalText.setJwtTitle)}
174
177
  action={
175
178
  <button
179
+ type="button"
176
180
  className={buttonRecipe({ variant: "primary" }, "w-full")}
177
181
  onClick={() => {
178
182
  st.set(
@@ -70,6 +70,7 @@ const DefaultToggleSelect = <I extends string | number | boolean | null>({
70
70
  <Fragment key={idx}>{renderItem(option, { selected: isSelected, disabled: isDisabled, onToggle })}</Fragment>
71
71
  ) : (
72
72
  <button
73
+ type="button"
73
74
  key={idx}
74
75
  disabled={isDisabled}
75
76
  className={cn(toggleBtn, isSelected && selectedCls, isDisabled && "cursor-not-allowed", btnClassName)}
@@ -128,6 +129,7 @@ const DefaultMulti = ({
128
129
  const isDisabled = (disabled ?? false) || (option.disabled ?? false);
129
130
  return (
130
131
  <button
132
+ type="button"
131
133
  key={idx}
132
134
  disabled={isDisabled}
133
135
  className={cn(toggleBtn, isSelected && selectedCls, isDisabled && "cursor-not-allowed", btnClassName)}