@antscorp/ui 0.1.2 → 0.1.4

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.
Files changed (99) hide show
  1. package/README.md +26 -2
  2. package/dist/atoms/AddButton.d.ts +3 -6
  3. package/dist/atoms/Chip.d.ts +2 -5
  4. package/dist/atoms/Chip.js +1 -1
  5. package/dist/atoms/InfoTip.js +2 -0
  6. package/dist/atoms/LabeledDivider.d.ts +1 -1
  7. package/dist/atoms/PanelSearchInput.d.ts +1 -1
  8. package/dist/atoms/RemoveButton.d.ts +2 -5
  9. package/dist/atoms/TextAction.d.ts +5 -6
  10. package/dist/atoms/TruncatedOption.d.ts +1 -1
  11. package/dist/cli.js +248 -0
  12. package/dist/doc/schema.d.ts +34 -0
  13. package/dist/doc/schema.js +33 -0
  14. package/dist/docs.json +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +53 -51
  17. package/dist/lib/iso.d.ts +2 -2
  18. package/dist/lib/iso.js +16 -8
  19. package/dist/lib/tokenEditorExtensions.js +1 -1
  20. package/dist/lib/useLoadingShown.d.ts +1 -1
  21. package/dist/molecules/AccentCard.d.ts +2 -3
  22. package/dist/molecules/AccentCard.js +2 -2
  23. package/dist/molecules/CheckRow.d.ts +3 -8
  24. package/dist/molecules/ChoiceTiles.d.ts +1 -1
  25. package/dist/molecules/ChoiceTiles.js +1 -1
  26. package/dist/molecules/CommitTextField.d.ts +1 -19
  27. package/dist/molecules/CommitTextField.js +21 -18
  28. package/dist/molecules/ConfirmDialog.d.ts +2 -7
  29. package/dist/molecules/DateTimeInput.d.ts +6 -3
  30. package/dist/molecules/DateTimeInput.js +82 -73
  31. package/dist/molecules/FieldGroup.d.ts +1 -1
  32. package/dist/molecules/FieldRow.d.ts +1 -1
  33. package/dist/molecules/MetaTable.d.ts +1 -1
  34. package/dist/molecules/PhoneFrame.d.ts +1 -1
  35. package/dist/molecules/RemovableRow.d.ts +1 -1
  36. package/dist/molecules/RemovableRow.js +32 -32
  37. package/dist/molecules/RevealRadioGroup.d.ts +2 -9
  38. package/dist/molecules/RevealSwitch.d.ts +2 -7
  39. package/dist/molecules/Select.d.ts +8 -24
  40. package/dist/molecules/Select.js +50 -44
  41. package/dist/molecules/SideLabelRow.d.ts +1 -1
  42. package/dist/molecules/SplitButton.d.ts +1 -1
  43. package/dist/molecules/StatusButton.d.ts +2 -7
  44. package/dist/molecules/TagSelect.d.ts +2 -2
  45. package/dist/molecules/TransferLayout.d.ts +1 -1
  46. package/dist/molecules/TreeOptions.d.ts +4 -3
  47. package/dist/molecules/TreeOptions.js +67 -44
  48. package/dist/organisms/HourOfDayRow.js +2 -2
  49. package/dist/organisms/HourOfWeekGrid.js +2 -2
  50. package/dist/organisms/RuleGroups.d.ts +3 -3
  51. package/dist/organisms/SearchableGroupedList.d.ts +1 -1
  52. package/dist/organisms/TransferPicker.d.ts +1 -1
  53. package/dist/organisms/TreeSearchPanel.js +50 -47
  54. package/dist/organisms/TreeSelect.d.ts +3 -3
  55. package/dist/ui.css +1 -1
  56. package/package.json +36 -19
  57. package/src/atoms/AddButton.tsx +3 -7
  58. package/src/atoms/Chip.tsx +7 -9
  59. package/src/atoms/InfoTip.tsx +2 -2
  60. package/src/atoms/LabeledDivider.tsx +1 -1
  61. package/src/atoms/PanelSearchInput.tsx +1 -1
  62. package/src/atoms/RemoveButton.tsx +2 -6
  63. package/src/atoms/TextAction.tsx +6 -8
  64. package/src/atoms/TruncatedOption.tsx +1 -1
  65. package/src/doc/schema.ts +67 -0
  66. package/src/index.ts +2 -0
  67. package/src/lib/iso.ts +15 -3
  68. package/src/lib/tokenEditorExtensions.ts +1 -1
  69. package/src/lib/useLoadMore.ts +1 -2
  70. package/src/lib/useLoadingShown.ts +1 -1
  71. package/src/molecules/AccentCard.tsx +11 -10
  72. package/src/molecules/CheckRow.tsx +3 -9
  73. package/src/molecules/ChoiceTiles.tsx +6 -4
  74. package/src/molecules/CommitTextField.tsx +15 -20
  75. package/src/molecules/ConfirmDialog.tsx +2 -8
  76. package/src/molecules/DateTimeInput.tsx +86 -68
  77. package/src/molecules/DurationInput.tsx +2 -2
  78. package/src/molecules/FieldGroup.tsx +1 -1
  79. package/src/molecules/FieldRow.tsx +1 -1
  80. package/src/molecules/FrequencyLimitRow.tsx +1 -1
  81. package/src/molecules/MetaTable.tsx +1 -1
  82. package/src/molecules/PhoneFrame.tsx +2 -2
  83. package/src/molecules/RemovableRow.tsx +3 -3
  84. package/src/molecules/RevealRadioGroup.tsx +5 -10
  85. package/src/molecules/RevealSwitch.tsx +2 -8
  86. package/src/molecules/Select.tsx +23 -23
  87. package/src/molecules/SideLabelRow.tsx +2 -2
  88. package/src/molecules/SplitButton.tsx +1 -1
  89. package/src/molecules/StatusButton.tsx +2 -8
  90. package/src/molecules/TagSelect.tsx +5 -5
  91. package/src/molecules/TransferLayout.tsx +2 -3
  92. package/src/molecules/TreeOptions.tsx +89 -65
  93. package/src/organisms/HourOfDayRow.tsx +5 -3
  94. package/src/organisms/HourOfWeekGrid.tsx +5 -3
  95. package/src/organisms/RuleGroups.tsx +3 -3
  96. package/src/organisms/SearchableGroupedList.tsx +2 -2
  97. package/src/organisms/TransferPicker.tsx +3 -3
  98. package/src/organisms/TreeSearchPanel.tsx +2 -2
  99. package/src/organisms/TreeSelect.tsx +4 -4
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @antscorp/ui
2
2
 
3
- React 19 views, Astryx, StyleX and the Antsomi theme through one facade.
3
+ React >=19.2 views, Astryx, StyleX and the Antsomi theme through one facade.
4
+ React and React DOM >=19.2 are supplied by the host as peers.
5
+ The packaged CLI requires Node >=24.
4
6
 
5
7
  ```sh
6
8
  bun add @antscorp/ui
@@ -23,5 +25,27 @@ export function Example() {
23
25
  ```
24
26
 
25
27
  StyleX consumers select `antscorp-source` and extract their own CSS instead of importing
26
- `ui.css`. See the [repository README](https://bitbucket.org/git-vn/at.antsomi-ui/src/main/README.md)
28
+ `ui.css`. See the [repository README](../../README.md)
27
29
  for source configuration, linked development and releases.
30
+
31
+ ## Localization
32
+
33
+ The host translates kit `label`/`labels`, descriptions and errors. Astryx-owned calendar,
34
+ clear, search and status text uses `InternationalizationProvider` from the same facade:
35
+
36
+ ```tsx
37
+ import { getLocaleDirection, InternationalizationProvider } from "@antscorp/ui/astryx/i18n";
38
+ import arSA from "@antscorp/ui/astryx/locales/ar-SA.generated.js";
39
+
40
+ const locale = "ar-SA";
41
+ const dir = getLocaleDirection(locale);
42
+
43
+ // The host also sets <html dir={dir}>; this provider renders no DOM element.
44
+ <InternationalizationProvider locale={locale} messages={{ [locale]: arSA }} dir={dir}>
45
+ <AntsomiTheme>{/* Views with host-translated labels. */}</AntsomiTheme>
46
+ </InternationalizationProvider>;
47
+ ```
48
+
49
+ Keep the DOM `dir` and provider direction in sync. Use `overrides` for sparse, locale-keyed
50
+ upstream text changes; missing translations fall back to English. `DateTimeInput` defaults to
51
+ `hourFormat="12h"`; set `hourFormat="24h"` for a 24-hour clock. No second translation layer is needed.
@@ -1,18 +1,15 @@
1
+ import { type ButtonProps } from "@astryxdesign/core/Button";
1
2
  import type { ReactNode } from "react";
2
3
  /** What `AddButton` takes. */
3
- export type AddButtonProps = {
4
- /** What it adds, such as "Add row"; the host translates it. */
5
- readonly label: string;
4
+ export type AddButtonProps = Pick<ButtonProps, "label" | "isDisabled"> & {
6
5
  /** `ghost` by default; `secondary` for an add that needs more weight. */
7
6
  readonly variant?: "ghost" | "secondary" | undefined;
8
7
  /** The button's height, `md` by default; its plus follows it. */
9
8
  readonly size?: "sm" | "md" | undefined;
10
9
  /** `default` keeps the button's own corners; `pill` rounds them fully; the theme colours its outline. */
11
10
  readonly shape?: "default" | "pill" | undefined;
12
- /** `sentence` (default) shows the label as given; `upper` in capitals, as fe-app's "+ AND". */
11
+ /** `sentence` (default) shows the label as given; `upper` in capitals, such as "+ AND". */
13
12
  readonly case?: "sentence" | "upper" | undefined;
14
- /** Dimmed and not pressable, e.g. when a limit is reached. */
15
- readonly isDisabled?: boolean | undefined;
16
13
  /** Read-only: an add is an editing affordance, so nothing is drawn. */
17
14
  readonly isReadOnly?: boolean | undefined;
18
15
  /** Called when the button is pressed. */
@@ -1,8 +1,7 @@
1
+ import { type TokenProps } from "@astryxdesign/core/Token";
1
2
  import type { ReactNode } from "react";
2
3
  /** What `Chip` takes. */
3
- export type ChipProps = {
4
- /** The chip's text; cut short with an ellipsis past `maxWidth`, and shown whole in a tooltip. */
5
- readonly label: string;
4
+ export type ChipProps = Pick<TokenProps, "label" | "isDisabled"> & {
6
5
  /** `neutral` (default) is the plain grey chip; `accent` the pale accent one; `danger` a value no longer valid. */
7
6
  readonly tone?: "neutral" | "accent" | "danger" | undefined;
8
7
  /** The chip's height, `sm` or `md` (default), as the controls beside it. */
@@ -21,8 +20,6 @@ export type ChipProps = {
21
20
  * button over its end; it stays in the tab order. Without it the button always shows after the text.
22
21
  */
23
22
  readonly isRemoveOnHover?: boolean | undefined;
24
- /** Dims the chip and blocks its remove button. */
25
- readonly isDisabled?: boolean | undefined;
26
23
  /** Read-only: no remove button. */
27
24
  readonly isReadOnly?: boolean | undefined;
28
25
  };
@@ -77,7 +77,7 @@ var l = {
77
77
  $$css: !0
78
78
  },
79
79
  accent: {
80
- kWkggS: "aui1gbsizx",
80
+ kWkggS: "auisqkk90",
81
81
  kMwMTN: "aui1tgivj0",
82
82
  $$css: !0
83
83
  },
@@ -17,6 +17,7 @@ function a(a) {
17
17
  let m;
18
18
  s[5] !== c || s[6] !== p ? (m = /* @__PURE__ */ t(i, {
19
19
  as: "span",
20
+ tabIndex: 0,
20
21
  color: "accent",
21
22
  "aria-label": c,
22
23
  xstyle: o.info,
@@ -25,6 +26,7 @@ function a(a) {
25
26
  let h;
26
27
  return s[8] !== c || s[9] !== m ? (h = /* @__PURE__ */ t(r, {
27
28
  content: c,
29
+ focusTrigger: "always",
28
30
  children: m
29
31
  }), s[8] = c, s[9] = m, s[10] = h) : h = s[10], h;
30
32
  }
@@ -3,7 +3,7 @@ import type { ReactNode } from "react";
3
3
  export type LabeledDividerProps = {
4
4
  /** The short word on the rule, such as "or"; it also names the separator. */
5
5
  readonly label: string;
6
- /** `sentence` (default) shows the word as given; `upper` in capitals, as fe-app's "OR". */
6
+ /** `sentence` (default) shows the word as given; `upper` in capitals, such as "OR". */
7
7
  readonly case?: "sentence" | "upper" | undefined;
8
8
  };
9
9
  /**
@@ -14,7 +14,7 @@ export type PanelSearchInputProps = {
14
14
  /** The clear button's name, such as "Clear search"; it shows only an icon. */
15
15
  readonly clear: string;
16
16
  };
17
- /** Where the magnifier sits: `start` (default) or `end`, after the clear button, as fe-app's search. */
17
+ /** Where the magnifier sits: `start` (default) or `end`, after the clear button. */
18
18
  readonly iconPosition?: "start" | "end" | undefined;
19
19
  /** The input element, to focus it (with `useFocusLater` in a pop-over, say). */
20
20
  readonly ref?: Ref<HTMLInputElement> | undefined;
@@ -1,12 +1,9 @@
1
+ import { type IconButtonProps } from "@astryxdesign/core/IconButton";
1
2
  import type { ReactNode } from "react";
2
3
  /** What `RemoveButton` takes. */
3
- export type RemoveButtonProps = {
4
- /** What goes, such as "Remove row"; the button shows only an icon, so this is its name. */
5
- readonly label: string;
4
+ export type RemoveButtonProps = Pick<IconButtonProps, "label" | "isDisabled"> & {
6
5
  /** The button's height, `md` by default. */
7
6
  readonly size?: "sm" | "md" | undefined;
8
- /** Dimmed and not pressable. */
9
- readonly isDisabled?: boolean | undefined;
10
7
  /** Read-only: a remove is an editing affordance, so nothing is drawn. */
11
8
  readonly isReadOnly?: boolean | undefined;
12
9
  /** Called when the button is pressed. */
@@ -1,11 +1,12 @@
1
+ import { type LinkProps } from "@astryxdesign/core/Link";
1
2
  import type { ReactNode } from "react";
2
3
  /** What `TextAction` takes. */
3
- export type TextActionProps = {
4
+ export type TextActionProps = Pick<LinkProps, "isDisabled"> & {
4
5
  /** What it does, such as "Remove all"; shown in capitals by default, so pass it in normal case. */
5
6
  readonly label: string;
6
7
  /** A glyph before the text, in the text's colour, such as an Astryx `Icon` of a bin for a removal. */
7
8
  readonly icon?: ReactNode;
8
- /** `upper` (default) shows the label in capitals, as fe-app's "REMOVE ALL"; `sentence` as given, as fe-app's text buttons. */
9
+ /** `upper` (default) shows the label in capitals, such as "REMOVE ALL"; `sentence` as given. */
9
10
  readonly case?: "upper" | "sentence" | undefined;
10
11
  /**
11
12
  * Called when it is pressed. Leave it out when a wrapper handles the presses, such as an Astryx
@@ -14,12 +15,10 @@ export type TextActionProps = {
14
15
  readonly onClick?: (() => void) | undefined;
15
16
  /** A down chevron after the text, for an action that opens a menu. */
16
17
  readonly hasChevron?: boolean | undefined;
17
- /** Dimmed and not pressable. */
18
- readonly isDisabled?: boolean | undefined;
19
18
  };
20
19
  /**
21
- * An action drawn as text alone: bold capitals in the accent, no frame, as fe-app's "REMOVE ALL"
22
- * and "CREATE NEW … ▾", or in sentence case with a glyph before it, as fe-app's text buttons
20
+ * An action drawn as text alone: bold capitals in the accent, no frame, such as "REMOVE ALL"
21
+ * and "CREATE NEW … ▾", or in sentence case with a glyph before it
23
22
  * (a bin, then "Remove"). An Astryx `Link` without an `href`, so a `<button>` in a link's look, with
24
23
  * a down chevron after the text when it opens a menu; still a button without `onClick`, so it can
25
24
  * be a `Popover`'s trigger. It holds no state.
@@ -7,7 +7,7 @@ export type TruncatedOptionProps = {
7
7
  };
8
8
  /**
9
9
  * A Selector option whose label ends in an ellipsis when it is wider than the pop-up, with the
10
- * whole label in a tooltip on hover, as antd's Select. Its icon and description show as the
10
+ * whole label in a tooltip on hover. Its icon and description show as the
11
11
  * Selector's own option's. It holds no state.
12
12
  */
13
13
  export declare function TruncatedOption({ option }: TruncatedOptionProps): ReactNode;
package/dist/cli.js ADDED
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/main.ts
4
+ import { readFileSync } from "node:fs";
5
+
6
+ // src/cli/catalog.ts
7
+ var record = (value) => typeof value === "object" && value !== null;
8
+ var texts = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
9
+ var objects = (value, check) => Array.isArray(value) && value.every((item) => record(item) && check(item));
10
+ var optional = (value, check) => value === undefined || check(value);
11
+ var text = (value) => typeof value === "string";
12
+ function component(value) {
13
+ return ["name", "level", "category", "import", "description", "usage", "primaryExample"].every((key) => text(value[key])) && texts(value["keywords"]) && objects(value["bestPractices"], (item) => typeof item["guidance"] === "boolean" && text(item["description"])) && optional(value["anatomy"], (items) => objects(items, (item) => text(item["name"]) && typeof item["required"] === "boolean" && text(item["description"]))) && optional(value["accessibility"], texts) && optional(value["related"], (items) => objects(items, (item) => text(item["name"]) && text(item["when"]))) && objects(value["props"], (item) => text(item["path"]) && text(item["type"]) && typeof item["required"] === "boolean" && optional(item["requiredWhenParent"], (flag) => flag === true) && optional(item["default"], text) && text(item["description"])) && objects(value["examples"], (item) => ["name", "description", "source", "file"].every((key) => text(item[key])));
14
+ }
15
+ function isCatalog(value) {
16
+ return record(value) && value["schemaVersion"] === 1 && text(value["package"]) && text(value["version"]) && objects(value["components"], component);
17
+ }
18
+ function parseCatalog(source) {
19
+ const value = JSON.parse(source);
20
+ if (!isCatalog(value))
21
+ throw new Error("Invalid component catalog (expected schemaVersion 1). Reinstall @antscorp/ui.");
22
+ return value;
23
+ }
24
+
25
+ // src/cli/run.ts
26
+ import { parseArgs } from "node:util";
27
+
28
+ // src/cli/format.ts
29
+ var nextCommand = (name) => `bunx @antscorp/ui component ${name}`;
30
+ var line = (text) => text.replaceAll(/\s+/gu, " ").trim();
31
+ var propsFor = (view) => ({
32
+ requiredProps: view.props.filter((prop) => prop.required),
33
+ keyProps: view.props.filter((prop) => !prop.required && (!prop.path.includes(".") || prop.path.startsWith("labels.")))
34
+ });
35
+ var denseData = (catalog, view) => {
36
+ const props = propsFor(view);
37
+ const fact = (prop) => ({
38
+ path: prop.path,
39
+ type: prop.required ? prop.type : prop.type.replace(/ \| undefined$/u, ""),
40
+ description: line(prop.description),
41
+ default: prop.default,
42
+ requiredWhenParent: prop.requiredWhenParent
43
+ });
44
+ return {
45
+ name: view.name,
46
+ package: catalog.package,
47
+ version: catalog.version,
48
+ category: view.category,
49
+ import: view.import,
50
+ usage: line(view.usage),
51
+ related: view.related,
52
+ requiredProps: props.requiredProps.map(fact),
53
+ bestPractices: view.bestPractices,
54
+ keyProps: props.keyProps.map(fact),
55
+ primaryExample: view.primaryExample,
56
+ example: `${nextCommand(view.name)} --example ${view.primaryExample}`,
57
+ more: [
58
+ `${nextCommand(view.name)} --full`,
59
+ "bunx @antscorp/ui search <query>",
60
+ "bunx @antscorp/ui list"
61
+ ]
62
+ };
63
+ };
64
+ function propLine(prop) {
65
+ const parent = prop.path.slice(0, prop.path.lastIndexOf("."));
66
+ const state = prop.required ? "required" : prop.requiredWhenParent === true ? `required when ${parent} is set` : "optional";
67
+ return `- ${prop.path}: ${line(prop.type)} (${state}${prop.default === undefined ? "" : `; default: ${prop.default}`}) — ${line(prop.description)}`;
68
+ }
69
+ function keyPropLines(props) {
70
+ return props.flatMap((prop, index) => {
71
+ if (!prop.path.startsWith("labels."))
72
+ return [propLine(prop)];
73
+ return [
74
+ ...props[index - 1]?.path.startsWith("labels") === true ? [] : ["- labels fields (see required props):"],
75
+ ` ${propLine(prop)}`
76
+ ];
77
+ });
78
+ }
79
+ function formatComponent(catalog, view, full) {
80
+ const { requiredProps: required, keyProps } = propsFor(view);
81
+ return [
82
+ `${view.name} — ${catalog.package} v${catalog.version} — ${view.category}`,
83
+ view.import,
84
+ `Use: ${line(view.usage)}`,
85
+ ...view.related === undefined ? [] : ["Alternatives:", ...view.related.map((item) => `- ${item.name}: ${line(item.when)}`)],
86
+ "Required props:",
87
+ ...required.length === 0 ? ["(none)"] : required.map(propLine),
88
+ "Rules:",
89
+ ...view.bestPractices.map((item) => `- ${item.guidance ? "Do" : "Don't"}: ${line(item.description)}`),
90
+ full ? "All props:" : "Key props (nested labels follow their parent):",
91
+ ...full ? view.props.map(propLine) : keyPropLines(keyProps),
92
+ ...full && view.anatomy !== undefined ? [
93
+ "Anatomy:",
94
+ ...view.anatomy.map((item) => `- ${item.name} (${item.required ? "required" : "optional"}): ${line(item.description)}`)
95
+ ] : [],
96
+ ...full && view.accessibility !== undefined ? ["Accessibility:", ...view.accessibility.map((item) => `- ${line(item)}`)] : [],
97
+ `Primary example: ${view.primaryExample}`,
98
+ `Example: ${nextCommand(view.name)} --example ${view.primaryExample}`,
99
+ ...full ? ["Examples:", ...view.examples.map((item) => `- ${item.name}: ${line(item.description)}`)] : [],
100
+ `more: ${nextCommand(view.name)} --full`,
101
+ "more: bunx @antscorp/ui search <query> | bunx @antscorp/ui list"
102
+ ].join(`
103
+ `);
104
+ }
105
+ function exampleData(view, name) {
106
+ const example = view.examples.find((item) => item.name.toLowerCase() === name.toLowerCase());
107
+ if (example === undefined)
108
+ throw new Error(`Unknown example ${name} for ${view.name}. Examples: ${view.examples.map((item) => item.name).join(", ")}`);
109
+ return {
110
+ ...example,
111
+ notice: "Storybook source. This declaration requires the story module's imports, data and metadata; it is not a standalone consumer example."
112
+ };
113
+ }
114
+ function search(catalog, query) {
115
+ const needle = query.toLowerCase().trim();
116
+ const terms = needle.split(/\s+/u);
117
+ return catalog.components.map((view) => {
118
+ const name = view.name.toLowerCase();
119
+ const score = name === needle ? 1e5 : terms.reduce((total, term) => total + (name.includes(term) ? 1000 : view.keywords.some((word) => word.toLowerCase().includes(term)) ? 100 : view.category.toLowerCase().includes(term) ? 10 : view.usage.toLowerCase().includes(term) ? 1 : 0), 0);
120
+ return { view, score };
121
+ }).filter((item) => item.score > 0).toSorted((left, right) => right.score - left.score || left.view.name.localeCompare(right.view.name, "en")).slice(0, 5).map(({ view }) => ({
122
+ name: view.name,
123
+ category: view.category,
124
+ description: view.description,
125
+ next: nextCommand(view.name)
126
+ }));
127
+ }
128
+ function suggestions(catalog, name) {
129
+ function distance(candidate) {
130
+ let row = Array.from({ length: candidate.length + 1 }, (_, index) => index);
131
+ for (const [i, char] of name.toLowerCase().split("").entries()) {
132
+ const next = [i + 1];
133
+ for (const [j, other] of candidate.toLowerCase().split("").entries())
134
+ next.push(Math.min((next[j] ?? 0) + 1, (row[j + 1] ?? 0) + 1, (row[j] ?? 0) + Number(char !== other)));
135
+ row = next;
136
+ }
137
+ return row.at(-1) ?? 0;
138
+ }
139
+ return catalog.components.map((view) => ({ name: view.name, distance: distance(view.name) })).toSorted((left, right) => left.distance - right.distance || left.name.localeCompare(right.name, "en")).slice(0, 3).map((view) => view.name);
140
+ }
141
+
142
+ // src/cli/run.ts
143
+ var USAGE = `Usage: bunx @antscorp/ui <command> [--json]
144
+ component <Name> [--full | --example <Story>]
145
+ search <query> Up to five ranked views, with the next command
146
+ list Every view: name | category | description
147
+ --help Show this help`;
148
+ function componentOutput(catalog, names, full, exampleName) {
149
+ const [name] = names;
150
+ if (name === undefined || names.length !== 1)
151
+ throw new Error("component requires one view name.");
152
+ if (full && exampleName !== undefined)
153
+ throw new Error("Choose --full or --example.");
154
+ const view = catalog.components.find((item) => item.name.toLowerCase() === name.toLowerCase());
155
+ if (view === undefined)
156
+ throw new Error(`Unknown view ${name}. Close names: ${suggestions(catalog, name).join(", ")}`);
157
+ if (exampleName === undefined)
158
+ return {
159
+ data: full ? { package: catalog.package, version: catalog.version, ...view } : denseData(catalog, view),
160
+ text: formatComponent(catalog, view, full)
161
+ };
162
+ const example = exampleData(view, exampleName);
163
+ return {
164
+ data: example,
165
+ text: `${view.name} — ${example.name}
166
+ ${example.description}
167
+ ${example.file}
168
+ ${example.notice}
169
+
170
+ ${example.source}`
171
+ };
172
+ }
173
+ function searchOutput(catalog, words) {
174
+ const query = words.join(" ").trim();
175
+ if (query.length === 0)
176
+ throw new Error("search requires a query.");
177
+ const results = search(catalog, query);
178
+ return {
179
+ data: results,
180
+ text: results.length === 0 ? `No matches for ${JSON.stringify(query)}.` : results.map((view) => `${view.name} | ${view.category} | ${view.description}
181
+ next: ${view.next}`).join(`
182
+
183
+ `)
184
+ };
185
+ }
186
+ function run(catalog, args) {
187
+ let json = false;
188
+ try {
189
+ const { values, positionals } = parseArgs({
190
+ args: [...args],
191
+ allowPositionals: true,
192
+ options: {
193
+ full: { type: "boolean" },
194
+ example: { type: "string" },
195
+ json: { type: "boolean" },
196
+ help: { type: "boolean", short: "h" }
197
+ }
198
+ });
199
+ json = values.json === true;
200
+ const [command, ...rest] = positionals;
201
+ const success = (data, formatted) => ({
202
+ code: 0,
203
+ stdout: json ? JSON.stringify(data) : formatted,
204
+ stderr: ""
205
+ });
206
+ if (values.help === true || command === undefined)
207
+ return success({ usage: USAGE }, USAGE);
208
+ if (command !== "component" && (values.full === true || values.example !== undefined))
209
+ throw new Error("--full and --example apply only to component.");
210
+ if (command === "list") {
211
+ if (rest.length > 0)
212
+ throw new Error("list takes no arguments.");
213
+ const views = catalog.components.map((view) => ({
214
+ name: view.name,
215
+ category: view.category,
216
+ description: view.description
217
+ }));
218
+ return success(views, views.map((view) => `${view.name} | ${view.category} | ${view.description}`).join(`
219
+ `));
220
+ }
221
+ if (command === "search") {
222
+ const output = searchOutput(catalog, rest);
223
+ return success(output.data, output.text);
224
+ }
225
+ if (command !== "component")
226
+ throw new Error(`Unknown command ${command}.
227
+ ${USAGE}`);
228
+ const output = componentOutput(catalog, rest, values.full === true, values.example);
229
+ return success(output.data, output.text);
230
+ } catch (error) {
231
+ const message = error instanceof Error ? error.message : String(error);
232
+ return { code: 1, stdout: "", stderr: json ? JSON.stringify({ error: message }) : message };
233
+ }
234
+ }
235
+
236
+ // src/cli/main.ts
237
+ try {
238
+ const catalog = parseCatalog(readFileSync(new URL("./docs.json", import.meta.url), "utf8"));
239
+ const result = run(catalog, process.argv.slice(2));
240
+ if (result.stdout.length > 0)
241
+ console.log(result.stdout);
242
+ if (result.stderr.length > 0)
243
+ console.error(result.stderr);
244
+ process.exitCode = result.code;
245
+ } catch (error) {
246
+ console.error(`Cannot read component catalog: ${error instanceof Error ? error.message : String(error)}`);
247
+ process.exitCode = 1;
248
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ /** Structured guidance beside a view; its props and examples come from TypeScript and stories. */
3
+ export declare const componentDocSchema: z.ZodObject<{
4
+ category: z.ZodEnum<{
5
+ "Data display": "Data display";
6
+ Feedback: "Feedback";
7
+ Inputs: "Inputs";
8
+ Layout: "Layout";
9
+ Overlays: "Overlays";
10
+ Selection: "Selection";
11
+ Theme: "Theme";
12
+ }>;
13
+ keywords: z.ZodArray<z.ZodString>;
14
+ related: z.ZodOptional<z.ZodArray<z.ZodObject<{
15
+ name: z.ZodString;
16
+ when: z.ZodString;
17
+ }, z.core.$strip>>>;
18
+ example: z.ZodOptional<z.ZodString>;
19
+ usage: z.ZodObject<{
20
+ description: z.ZodString;
21
+ bestPractices: z.ZodArray<z.ZodObject<{
22
+ guidance: z.ZodBoolean;
23
+ description: z.ZodString;
24
+ }, z.core.$strip>>;
25
+ anatomy: z.ZodOptional<z.ZodArray<z.ZodObject<{
26
+ name: z.ZodString;
27
+ required: z.ZodBoolean;
28
+ description: z.ZodString;
29
+ }, z.core.$strip>>>;
30
+ }, z.core.$strip>;
31
+ accessibility: z.ZodOptional<z.ZodArray<z.ZodString>>;
32
+ }, z.core.$strip>;
33
+ /** Authoring contract for a view's `<View>.doc.ts` export. */
34
+ export type ComponentDoc = z.input<typeof componentDocSchema>;
@@ -0,0 +1,33 @@
1
+ import { z as e } from "zod";
2
+ // src/doc/schema.ts
3
+ var t = e.string().trim().min(1), n = e.object({
4
+ category: e.enum([
5
+ "Inputs",
6
+ "Selection",
7
+ "Feedback",
8
+ "Layout",
9
+ "Data display",
10
+ "Overlays",
11
+ "Theme"
12
+ ]).describe("The view's purpose: Inputs edits values; Selection chooses options; Feedback communicates status; Layout arranges content; Data display presents values; Overlays shows dialogs or panels; Theme provides visual tokens and icons."),
13
+ keywords: e.array(t).min(3).describe("At least three search terms for the view."),
14
+ related: e.array(e.object({
15
+ name: t.describe("A kit view or Astryx facade module, such as astryx/Badge."),
16
+ when: t.describe("When to choose this alternative.")
17
+ })).min(1).optional().describe("Alternatives, validated against public views and facade modules during docs generation."),
18
+ example: t.optional().describe("The existing story export shown first, validated during docs generation."),
19
+ usage: e.object({
20
+ description: t.describe("When and how to use the view."),
21
+ bestPractices: e.array(e.object({
22
+ guidance: e.boolean().describe("True for a recommended practice; false for a practice to avoid."),
23
+ description: t.describe("The practice and its reason.")
24
+ })).min(2).refine((e) => e.some((e) => e.guidance) && e.some((e) => !e.guidance), "Include a recommended practice and a practice to avoid.").describe("Recommended practices and practices to avoid."),
25
+ anatomy: e.array(e.object({
26
+ name: t.describe("The part's name."),
27
+ required: e.boolean().describe("Whether this part is always present."),
28
+ description: t.describe("The part's purpose and the condition when it appears.")
29
+ })).min(1).optional().describe("The view's visible and interactive parts.")
30
+ }).describe("Usage, best practices and optional anatomy."),
31
+ accessibility: e.array(t).min(1).optional().describe("Accessible naming and interaction guidance.")
32
+ });
33
+ export { n as componentDocSchema };