@flanksource/clicky-ui 0.3.6 → 0.3.8

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 (215) hide show
  1. package/README.md +42 -0
  2. package/dist/components/DateField.d.ts +1 -1
  3. package/dist/components/DatePicker.d.ts +1 -1
  4. package/dist/components/DateTimePicker.d.ts +1 -1
  5. package/dist/components/FormLookupProvider.cjs +12 -0
  6. package/dist/components/FormLookupProvider.cjs.map +1 -0
  7. package/dist/components/FormLookupProvider.d.ts +7 -0
  8. package/dist/components/FormLookupProvider.d.ts.map +1 -0
  9. package/dist/components/FormLookupProvider.js +12 -0
  10. package/dist/components/FormLookupProvider.js.map +1 -0
  11. package/dist/components/JsonSchemaForm.cjs +2 -2
  12. package/dist/components/JsonSchemaForm.cjs.map +1 -1
  13. package/dist/components/JsonSchemaForm.js +1 -1
  14. package/dist/components/JsonSchemaForm.js.map +1 -1
  15. package/dist/components/ListMenu.cjs +145 -0
  16. package/dist/components/ListMenu.cjs.map +1 -0
  17. package/dist/components/ListMenu.d.ts +54 -0
  18. package/dist/components/ListMenu.d.ts.map +1 -0
  19. package/dist/components/ListMenu.js +145 -0
  20. package/dist/components/ListMenu.js.map +1 -0
  21. package/dist/components/MdxEditorField.cjs +128 -0
  22. package/dist/components/MdxEditorField.cjs.map +1 -0
  23. package/dist/components/MdxEditorField.d.ts +4 -0
  24. package/dist/components/MdxEditorField.d.ts.map +1 -0
  25. package/dist/components/MdxEditorField.js +106 -0
  26. package/dist/components/MdxEditorField.js.map +1 -0
  27. package/dist/components/MdxEditorToolbar.cjs +76 -0
  28. package/dist/components/MdxEditorToolbar.cjs.map +1 -0
  29. package/dist/components/MdxEditorToolbar.d.ts +8 -0
  30. package/dist/components/MdxEditorToolbar.d.ts.map +1 -0
  31. package/dist/components/MdxEditorToolbar.js +76 -0
  32. package/dist/components/MdxEditorToolbar.js.map +1 -0
  33. package/dist/components/button-variants.d.ts +1 -1
  34. package/dist/components/button.d.ts +1 -1
  35. package/dist/components/form-lookup-context.cjs +1 -8
  36. package/dist/components/form-lookup-context.cjs.map +1 -1
  37. package/dist/components/form-lookup-context.d.ts +1 -5
  38. package/dist/components/form-lookup-context.d.ts.map +1 -1
  39. package/dist/components/form-lookup-context.js +1 -8
  40. package/dist/components/form-lookup-context.js.map +1 -1
  41. package/dist/components/jotai-bindings-core.cjs +197 -0
  42. package/dist/components/jotai-bindings-core.cjs.map +1 -0
  43. package/dist/components/jotai-bindings-core.d.ts +45 -0
  44. package/dist/components/jotai-bindings-core.d.ts.map +1 -0
  45. package/dist/components/jotai-bindings-core.js +197 -0
  46. package/dist/components/jotai-bindings-core.js.map +1 -0
  47. package/dist/components/jotai-bindings.cjs +17 -0
  48. package/dist/components/jotai-bindings.cjs.map +1 -0
  49. package/dist/components/jotai-bindings.d.ts +4 -0
  50. package/dist/components/jotai-bindings.d.ts.map +1 -0
  51. package/dist/components/jotai-bindings.js +17 -0
  52. package/dist/components/jotai-bindings.js.map +1 -0
  53. package/dist/components/json-schema-form-fields.cjs +37 -0
  54. package/dist/components/json-schema-form-fields.cjs.map +1 -1
  55. package/dist/components/json-schema-form-fields.d.ts +6 -0
  56. package/dist/components/json-schema-form-fields.d.ts.map +1 -1
  57. package/dist/components/json-schema-form-fields.js +38 -1
  58. package/dist/components/json-schema-form-fields.js.map +1 -1
  59. package/dist/components/json-schema-form-render.cjs +3 -1
  60. package/dist/components/json-schema-form-render.cjs.map +1 -1
  61. package/dist/components/json-schema-form-render.d.ts.map +1 -1
  62. package/dist/components/json-schema-form-render.js +4 -2
  63. package/dist/components/json-schema-form-render.js.map +1 -1
  64. package/dist/components/json-schema-form-resolve.cjs +9 -0
  65. package/dist/components/json-schema-form-resolve.cjs.map +1 -1
  66. package/dist/components/json-schema-form-resolve.d.ts.map +1 -1
  67. package/dist/components/json-schema-form-resolve.js +9 -0
  68. package/dist/components/json-schema-form-resolve.js.map +1 -1
  69. package/dist/components/json-schema-form-types.d.ts +4 -1
  70. package/dist/components/json-schema-form-types.d.ts.map +1 -1
  71. package/dist/components/mdx-editor-field-plugins.cjs +124 -0
  72. package/dist/components/mdx-editor-field-plugins.cjs.map +1 -0
  73. package/dist/components/mdx-editor-field-plugins.d.ts +6 -0
  74. package/dist/components/mdx-editor-field-plugins.d.ts.map +1 -0
  75. package/dist/components/mdx-editor-field-plugins.js +124 -0
  76. package/dist/components/mdx-editor-field-plugins.js.map +1 -0
  77. package/dist/components/mdx-editor-options.d.ts +77 -0
  78. package/dist/components/mdx-editor-options.d.ts.map +1 -0
  79. package/dist/components/use-list-menu-selection.cjs +77 -0
  80. package/dist/components/use-list-menu-selection.cjs.map +1 -0
  81. package/dist/components/use-list-menu-selection.d.ts +40 -0
  82. package/dist/components/use-list-menu-selection.d.ts.map +1 -0
  83. package/dist/components/use-list-menu-selection.js +77 -0
  84. package/dist/components/use-list-menu-selection.js.map +1 -0
  85. package/dist/components.cjs +10 -1
  86. package/dist/components.cjs.map +1 -1
  87. package/dist/components.d.ts +4 -1
  88. package/dist/components.d.ts.map +1 -1
  89. package/dist/components.js +10 -1
  90. package/dist/components.js.map +1 -1
  91. package/dist/data/MarkdownEditor.model.d.ts +1 -1
  92. package/dist/data/PromptBanner.cjs +199 -0
  93. package/dist/data/PromptBanner.cjs.map +1 -0
  94. package/dist/data/PromptBanner.d.ts +17 -0
  95. package/dist/data/PromptBanner.d.ts.map +1 -0
  96. package/dist/data/PromptBanner.js +199 -0
  97. package/dist/data/PromptBanner.js.map +1 -0
  98. package/dist/data/PromptBanner.model.cjs +132 -0
  99. package/dist/data/PromptBanner.model.cjs.map +1 -0
  100. package/dist/data/PromptBanner.model.d.ts +23 -0
  101. package/dist/data/PromptBanner.model.d.ts.map +1 -0
  102. package/dist/data/PromptBanner.model.js +132 -0
  103. package/dist/data/PromptBanner.model.js.map +1 -0
  104. package/dist/data/PromptDialog.cjs +43 -0
  105. package/dist/data/PromptDialog.cjs.map +1 -0
  106. package/dist/data/PromptDialog.d.ts +14 -0
  107. package/dist/data/PromptDialog.d.ts.map +1 -0
  108. package/dist/data/PromptDialog.js +43 -0
  109. package/dist/data/PromptDialog.js.map +1 -0
  110. package/dist/data/TaskManager.cjs +8 -1
  111. package/dist/data/TaskManager.cjs.map +1 -1
  112. package/dist/data/TaskManager.d.ts.map +1 -1
  113. package/dist/data/TaskManager.js +8 -1
  114. package/dist/data/TaskManager.js.map +1 -1
  115. package/dist/data/TaskProgress.cjs +15 -2
  116. package/dist/data/TaskProgress.cjs.map +1 -1
  117. package/dist/data/TaskProgress.d.ts +0 -10
  118. package/dist/data/TaskProgress.d.ts.map +1 -1
  119. package/dist/data/TaskProgress.js +15 -2
  120. package/dist/data/TaskProgress.js.map +1 -1
  121. package/dist/data/ai/ChatWindow.cjs +53 -9
  122. package/dist/data/ai/ChatWindow.cjs.map +1 -1
  123. package/dist/data/ai/ChatWindow.d.ts.map +1 -1
  124. package/dist/data/ai/ChatWindow.js +53 -9
  125. package/dist/data/ai/ChatWindow.js.map +1 -1
  126. package/dist/data/ai/ToolPreferences.cjs +232 -39
  127. package/dist/data/ai/ToolPreferences.cjs.map +1 -1
  128. package/dist/data/ai/ToolPreferences.d.ts +4 -7
  129. package/dist/data/ai/ToolPreferences.d.ts.map +1 -1
  130. package/dist/data/ai/ToolPreferences.js +234 -41
  131. package/dist/data/ai/ToolPreferences.js.map +1 -1
  132. package/dist/data/chat/clickyOperationsToTools.cjs +89 -3
  133. package/dist/data/chat/clickyOperationsToTools.cjs.map +1 -1
  134. package/dist/data/chat/clickyOperationsToTools.d.ts +1 -1
  135. package/dist/data/chat/clickyOperationsToTools.d.ts.map +1 -1
  136. package/dist/data/chat/clickyOperationsToTools.js +89 -3
  137. package/dist/data/chat/clickyOperationsToTools.js.map +1 -1
  138. package/dist/data/chat/types.cjs.map +1 -1
  139. package/dist/data/chat/types.d.ts +7 -1
  140. package/dist/data/chat/types.d.ts.map +1 -1
  141. package/dist/data/chat/types.js.map +1 -1
  142. package/dist/data/task-status.cjs +19 -12
  143. package/dist/data/task-status.cjs.map +1 -1
  144. package/dist/data/task-status.d.ts +2 -1
  145. package/dist/data/task-status.d.ts.map +1 -1
  146. package/dist/data/task-status.js +19 -12
  147. package/dist/data/task-status.js.map +1 -1
  148. package/dist/data/test-runner/TestRunner.fixtures.d.ts +11 -4
  149. package/dist/data/test-runner/TestRunner.fixtures.d.ts.map +1 -1
  150. package/dist/data/version-info.cjs +3 -3
  151. package/dist/data/version-info.js +3 -3
  152. package/dist/data.cjs +7 -0
  153. package/dist/data.cjs.map +1 -1
  154. package/dist/data.d.ts +3 -0
  155. package/dist/data.d.ts.map +1 -1
  156. package/dist/data.js +7 -0
  157. package/dist/data.js.map +1 -1
  158. package/dist/hooks/use-prompts.cjs +83 -0
  159. package/dist/hooks/use-prompts.cjs.map +1 -0
  160. package/dist/hooks/use-prompts.d.ts +43 -0
  161. package/dist/hooks/use-prompts.d.ts.map +1 -0
  162. package/dist/hooks/use-prompts.js +83 -0
  163. package/dist/hooks/use-prompts.js.map +1 -0
  164. package/dist/hooks.cjs +3 -0
  165. package/dist/hooks.cjs.map +1 -1
  166. package/dist/hooks.d.ts +1 -0
  167. package/dist/hooks.d.ts.map +1 -1
  168. package/dist/hooks.js +3 -0
  169. package/dist/hooks.js.map +1 -1
  170. package/dist/index.cjs +17 -1
  171. package/dist/index.cjs.map +1 -1
  172. package/dist/index.js +17 -1
  173. package/dist/index.js.map +1 -1
  174. package/dist/jotai.cjs +9 -0
  175. package/dist/jotai.cjs.map +1 -0
  176. package/dist/jotai.d.ts +4 -0
  177. package/dist/jotai.d.ts.map +1 -0
  178. package/dist/jotai.js +9 -0
  179. package/dist/jotai.js.map +1 -0
  180. package/dist/lib/utils.cjs +18 -1
  181. package/dist/lib/utils.cjs.map +1 -1
  182. package/dist/lib/utils.d.ts.map +1 -1
  183. package/dist/lib/utils.js +18 -1
  184. package/dist/lib/utils.js.map +1 -1
  185. package/dist/mdx-editor.cjs +5 -0
  186. package/dist/mdx-editor.cjs.map +1 -0
  187. package/dist/mdx-editor.css +2 -0
  188. package/dist/mdx-editor.d.ts +2 -0
  189. package/dist/mdx-editor.d.ts.map +1 -0
  190. package/dist/mdx-editor.js +5 -0
  191. package/dist/mdx-editor.js.map +1 -0
  192. package/dist/rpc/apiClient.cjs +1 -1
  193. package/dist/rpc/apiClient.cjs.map +1 -1
  194. package/dist/rpc/apiClient.js +1 -1
  195. package/dist/rpc/apiClient.js.map +1 -1
  196. package/dist/rpc/surfaceIconMap.cjs +12 -1
  197. package/dist/rpc/surfaceIconMap.cjs.map +1 -1
  198. package/dist/rpc/surfaceIconMap.d.ts.map +1 -1
  199. package/dist/rpc/surfaceIconMap.js +12 -1
  200. package/dist/rpc/surfaceIconMap.js.map +1 -1
  201. package/dist/rpc/types.cjs.map +1 -1
  202. package/dist/rpc/types.d.ts +1 -0
  203. package/dist/rpc/types.d.ts.map +1 -1
  204. package/dist/rpc/types.js.map +1 -1
  205. package/dist/styles.css +1 -1
  206. package/oxlint-plugins/README.md +148 -0
  207. package/oxlint-plugins/clicky-ui-no-adhoc-overlay.js +88 -0
  208. package/oxlint-plugins/clicky-ui-prefer-components.js +46 -0
  209. package/oxlint-plugins/clicky-ui-prefer-icons.js +121 -0
  210. package/oxlint-plugins/clicky-ui-prefer-tailwind.js +98 -0
  211. package/oxlint-plugins/clicky-ui-prefer-theme.js +134 -0
  212. package/oxlint-plugins/clicky-ui-shared.js +132 -0
  213. package/oxlint-plugins/clicky-ui.js +34 -0
  214. package/oxlint-plugins/no-iconify-names.js +37 -0
  215. package/package.json +24 -4
@@ -0,0 +1,148 @@
1
+ # clicky-ui lint rules
2
+
3
+ Custom [oxlint](https://oxc.rs/docs/guide/usage/linter/js-plugins.html) rules that
4
+ projects **consuming** `@flanksource/clicky-ui` can enable to keep their code on
5
+ the library's components, overlays, theming and icons instead of re-implementing
6
+ them by hand.
7
+
8
+ The rules ship as a JS plugin inside the published package
9
+ (`@flanksource/clicky-ui/oxlint-plugins`), so a downstream repo can turn them on
10
+ without copying anything.
11
+
12
+ ## Setup
13
+
14
+ These rules require oxlint's [JS plugins](https://oxc.rs/docs/guide/usage/linter/js-plugins.html)
15
+ (oxlint `>=1.50`, Node `>=20.19`). Add the plugin and the rules you want to your
16
+ downstream `.oxlintrc.json`:
17
+
18
+ ```json
19
+ {
20
+ "jsPlugins": [
21
+ { "name": "clicky-ui", "specifier": "@flanksource/clicky-ui/oxlint-plugins" }
22
+ ],
23
+ "rules": {
24
+ "clicky-ui/prefer-clicky-components": "warn",
25
+ "clicky-ui/no-adhoc-overlay": "error",
26
+ "clicky-ui/prefer-tailwind-classes": "warn",
27
+ "clicky-ui/prefer-theme-tokens": "error",
28
+ "clicky-ui/prefer-clicky-icons": "warn"
29
+ }
30
+ }
31
+ ```
32
+
33
+ The `specifier` form resolves through the package's `exports` map. If your
34
+ toolchain can't resolve it, point `jsPlugins` at the file directly instead:
35
+
36
+ ```json
37
+ {
38
+ "jsPlugins": ["./node_modules/@flanksource/clicky-ui/oxlint-plugins/clicky-ui.js"]
39
+ }
40
+ ```
41
+
42
+ Every rule is **opt-in** — nothing is enabled until you list it. Choose the
43
+ severity (`"warn"` / `"error"`) per rule.
44
+
45
+ ## Rules
46
+
47
+ | Rule | Flags | Use instead |
48
+ | --- | --- | --- |
49
+ | `clicky-ui/prefer-clicky-components` | Native `<button>`, `<select>`, `<table>`, `<dialog>` that re-implement a shipped component | `Button`/`IconButton`, `Select`/`Combobox`, `DataTable`, `Modal` |
50
+ | `clicky-ui/no-adhoc-overlay` | `role="dialog"`/`"alertdialog"`, arbitrary `z-[N]` classes, `style={{ zIndex: N }}` | `Modal`, `DropdownMenu`, `Toast`, and the `zIndex` scale |
51
+ | `clicky-ui/prefer-tailwind-classes` | Inline `style` with **static** spacing/layout/typography values (`padding`, `margin`, `gap`, `display`, `fontWeight`, …) | Tailwind utilities + density utilities (`p-density-4`, `gap-density-2`) |
52
+ | `clicky-ui/prefer-theme-tokens` | Hardcoded colors (`text-[#fff]`, `style={{ color: "#fff" }}`) and hand-reading the `clicky-ui-theme`/`clicky-ui-density` storage keys | Semantic tokens (`text-foreground`, `bg-background`, …) and the `useTheme`/`useDensity` hooks |
53
+ | `clicky-ui/prefer-clicky-icons` | Icon imports from third-party libraries (lucide, react-icons, heroicons, MUI, phosphor, tabler, font-awesome, …), emoji used as icons, and raw iconify name strings (`codicon:clock`, `lucide:activity`, …) passed as a value/argument | `@flanksource/clicky-ui/icons` (`Ui*`), `@flanksource/icons/mi`, or `@iconify/react` |
54
+
55
+ ### `prefer-clicky-components`
56
+
57
+ Rebuilding a primitive by hand means re-doing its styling, theming, density and
58
+ accessibility — and drifting from the library over time. Reuse the component.
59
+
60
+ ```tsx
61
+ // ✗ flagged
62
+ <button onClick={save}>Save</button>;
63
+
64
+ // ✓
65
+ import { Button } from "@flanksource/clicky-ui/components";
66
+ <Button onClick={save}>Save</Button>;
67
+ ```
68
+
69
+ ### `no-adhoc-overlay`
70
+
71
+ Overlays must share clicky-ui's centralized z-index scale (`overlay/zIndex.ts`)
72
+ so a dropdown opened inside a modal can't render behind it. Arbitrary `z-[N]`
73
+ Tailwind classes are doubly bad: a consumer's Tailwind build only emits the
74
+ arbitrary classes it can scan from the published dist, so a fresh `z-[N]` may
75
+ compile to nothing.
76
+
77
+ ```tsx
78
+ // ✗ flagged
79
+ <div role="dialog" className="z-[9999]">…</div>;
80
+
81
+ // ✓
82
+ import { Modal } from "@flanksource/clicky-ui/components";
83
+ <Modal open={open} onClose={close}>…</Modal>;
84
+ ```
85
+
86
+ ### `prefer-tailwind-classes`
87
+
88
+ Static inline styles bypass Tailwind and the density system. **Computed** values
89
+ (`var()`, `calc()`, `clamp()`, identifiers, expressions) are intentionally
90
+ allowed — those legitimately can't be a static class.
91
+
92
+ ```tsx
93
+ // ✗ flagged
94
+ <div style={{ padding: 8, gap: 4 }} />;
95
+
96
+ // ✓
97
+ <div className="p-density-4 gap-density-2" />;
98
+
99
+ // ✓ allowed — dynamic value can't be a static class
100
+ <div style={{ fontSize: "clamp(1rem, 18cqmin, 2.5rem)" }} />;
101
+ ```
102
+
103
+ ### `prefer-theme-tokens`
104
+
105
+ Hardcoded colors don't flip with `[data-theme]`, defeating light/dark theming.
106
+ Reading the storage keys by hand re-implements the hooks.
107
+
108
+ ```tsx
109
+ // ✗ flagged
110
+ <span className="text-[#111]" style={{ backgroundColor: "#fff" }} />;
111
+ localStorage.getItem("clicky-ui-theme");
112
+
113
+ // ✓
114
+ <span className="text-foreground bg-background" />;
115
+ import { useTheme } from "@flanksource/clicky-ui/hooks";
116
+ const { theme } = useTheme();
117
+ ```
118
+
119
+ ### `prefer-clicky-icons`
120
+
121
+ Icons should come from an approved source and be real components, not emoji,
122
+ glyphs pulled from an unrelated icon library, or raw iconify name strings
123
+ (known icon-set prefixes: `ph`, `lucide`, `codicon`, `tabler`, `mdi`,
124
+ `svg-spinners`, `jb-expui-*`) passed as a value or argument.
125
+
126
+ ```tsx
127
+ // ✗ flagged
128
+ import { Star } from "lucide-react";
129
+ <span>🔍</span>;
130
+ const icon = "codicon:clock"; // raw iconify name string
131
+ <Icon icon="lucide:activity" />;
132
+
133
+ // ✓
134
+ import { UiStar } from "@flanksource/clicky-ui/icons";
135
+ import { K8SSecret } from "@flanksource/icons/mi";
136
+ import { Icon } from "@iconify/react";
137
+ ```
138
+
139
+ ## Developing the rules
140
+
141
+ Each rule lives in its own `clicky-ui-*.js` file and exports a pure predicate
142
+ (e.g. `isArbitraryZIndexClass`, `isUnapprovedIconImport`) that is unit-tested in
143
+ `clicky-ui.test.ts`. `clicky-ui.js` is the plugin entry that registers them. Run
144
+ the tests with:
145
+
146
+ ```bash
147
+ pnpm --filter @flanksource/clicky-ui test clicky-ui
148
+ ```
@@ -0,0 +1,88 @@
1
+ // clicky-ui/no-adhoc-overlay — keep overlays going through clicky-ui's Modal and
2
+ // its centralized z-index scale (overlay/zIndex.ts) instead of hand-rolled
3
+ // dialogs and arbitrary stacking values that silently fight the modal stack.
4
+
5
+ import {
6
+ attributeName,
7
+ classTokens,
8
+ collectStrings,
9
+ numericLiteral,
10
+ propertyKey,
11
+ styleObject,
12
+ } from "./clicky-ui-shared.js";
13
+
14
+ // `z-[999]`, `hover:z-[999]`, `md:z-[var(--x)]` — an arbitrary Tailwind z-index.
15
+ // Named utilities (`z-50`) are fine; only escape-hatch arbitrary values are flagged.
16
+ const ARBITRARY_Z_CLASS = /(?:^|:)z-\[[^\]]+\]$/;
17
+ const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
18
+
19
+ /** True for an arbitrary `z-[…]` Tailwind class token (variant prefixes allowed). */
20
+ export function isArbitraryZIndexClass(token) {
21
+ return typeof token === "string" && ARBITRARY_Z_CLASS.test(token);
22
+ }
23
+
24
+ /** True for a `role` value that turns a plain element into an ad-hoc dialog. */
25
+ export function isDialogRole(value) {
26
+ return typeof value === "string" && DIALOG_ROLES.has(value);
27
+ }
28
+
29
+ const Z_INDEX_MESSAGE =
30
+ "Hardcoded z-index. Use clicky-ui's overlay components (Modal, DropdownMenu, " +
31
+ "Toast) or the `zIndex` scale from @flanksource/clicky-ui so stacking stays " +
32
+ "consistent — arbitrary z-[N] classes can also be dropped by a consumer's " +
33
+ "Tailwind build.";
34
+
35
+ function checkClassName(node, context) {
36
+ if (attributeName(node) !== "className" && attributeName(node) !== "class") return;
37
+ for (const token of classTokens(collectStrings(node.value))) {
38
+ if (isArbitraryZIndexClass(token)) {
39
+ context.report({ message: Z_INDEX_MESSAGE, node });
40
+ return;
41
+ }
42
+ }
43
+ }
44
+
45
+ function checkStyleZIndex(node, context) {
46
+ if (attributeName(node) !== "style") return;
47
+ const object = styleObject(node);
48
+ if (!object) return;
49
+ for (const prop of object.properties) {
50
+ if (propertyKey(prop) === "zIndex" && numericLiteral(prop.value) !== undefined) {
51
+ context.report({ message: Z_INDEX_MESSAGE, node: prop });
52
+ }
53
+ }
54
+ }
55
+
56
+ function checkDialogRole(node, context) {
57
+ if (attributeName(node) !== "role") return;
58
+ const value = node.value?.type === "Literal" ? node.value.value : undefined;
59
+ if (isDialogRole(value)) {
60
+ context.report({
61
+ message:
62
+ `role="${value}" builds an ad-hoc dialog. Use clicky-ui's Modal, which ` +
63
+ "owns focus trapping and the modal z-index stack.",
64
+ node,
65
+ });
66
+ }
67
+ }
68
+
69
+ const rule = {
70
+ meta: {
71
+ type: "problem",
72
+ docs: {
73
+ description:
74
+ "Use clicky-ui overlays and the centralized z-index scale instead of ad-hoc dialogs",
75
+ },
76
+ },
77
+ create(context) {
78
+ return {
79
+ JSXAttribute(node) {
80
+ checkClassName(node, context);
81
+ checkStyleZIndex(node, context);
82
+ checkDialogRole(node, context);
83
+ },
84
+ };
85
+ },
86
+ };
87
+
88
+ export default rule;
@@ -0,0 +1,46 @@
1
+ // clicky-ui/prefer-clicky-components — flag native DOM elements that re-implement
2
+ // a component clicky-ui already ships, so consumers reuse the library instead of
3
+ // rebuilding (and re-styling, re-theming) primitives by hand.
4
+
5
+ import { intrinsicElementName } from "./clicky-ui-shared.js";
6
+
7
+ // Intrinsic element → the clicky-ui component(s) that should replace it.
8
+ const REPLACEMENTS = {
9
+ button: "Button or IconButton (@flanksource/clicky-ui/components)",
10
+ select: "Select or Combobox (@flanksource/clicky-ui/components)",
11
+ table: "DataTable (@flanksource/clicky-ui/data)",
12
+ dialog: "Modal (@flanksource/clicky-ui/components)",
13
+ };
14
+
15
+ /** The clicky-ui replacement for an intrinsic element name, else undefined. */
16
+ export function clickyReplacementFor(elementName) {
17
+ if (typeof elementName !== "string") return undefined;
18
+ return REPLACEMENTS[elementName];
19
+ }
20
+
21
+ const rule = {
22
+ meta: {
23
+ type: "suggestion",
24
+ docs: {
25
+ description:
26
+ "Reuse clicky-ui components instead of rebuilding native DOM elements by hand",
27
+ },
28
+ },
29
+ create(context) {
30
+ return {
31
+ JSXOpeningElement(node) {
32
+ const element = intrinsicElementName(node);
33
+ const replacement = clickyReplacementFor(element);
34
+ if (!replacement) return;
35
+ context.report({
36
+ message:
37
+ `Avoid rebuilding <${element}>. Reuse ${replacement} so styling, ` +
38
+ "theming, density and accessibility stay consistent with clicky-ui.",
39
+ node,
40
+ });
41
+ },
42
+ };
43
+ },
44
+ };
45
+
46
+ export default rule;
@@ -0,0 +1,121 @@
1
+ // clicky-ui/prefer-clicky-icons — icons must come from an approved source, and
2
+ // must be real icon components rather than emoji/text "labels".
3
+ //
4
+ // Approved sources:
5
+ // - @flanksource/clicky-ui/icons (generated Ui* components, e.g. UiFullscreen)
6
+ // - @flanksource/icons/mi (Material/brand glyphs)
7
+ // - @iconify/react (the <Icon> primitive / iconify icons)
8
+ //
9
+ // Anti-patterns this flags:
10
+ // 1. Importing icons from any other icon library (lucide, react-icons,
11
+ // heroicons, @mui/icons-material, phosphor, tabler, font-awesome, …).
12
+ // 2. Using an emoji as an icon — a bare emoji JSX child, or an emoji passed to
13
+ // an `icon` prop — instead of a real icon component.
14
+ // 3. Passing a raw iconify name string (e.g. "codicon:clock", "lucide:activity")
15
+ // as a value/argument instead of importing the generated Ui* component.
16
+
17
+ import { attributeName, isIconifyName, literalString } from "./clicky-ui-shared.js";
18
+
19
+ // Sources that are always fine (never flagged), even if a denylist pattern overlaps.
20
+ const APPROVED_SOURCES = [
21
+ /^@iconify\/react(\/|$)/,
22
+ /^@iconify-icons\//,
23
+ /^@flanksource\/clicky-ui\/icons(\/|$)/,
24
+ /^@flanksource\/icons(\/|$)/,
25
+ ];
26
+
27
+ // Third-party icon libraries consumers should not pull icons from directly.
28
+ const ICON_LIBRARIES = [
29
+ /^lucide-react$/,
30
+ /^lucide$/,
31
+ /^react-icons(\/|$)/,
32
+ /^@heroicons\/react(\/|$)/,
33
+ /^@mui\/icons-material(\/|$)/,
34
+ /^@phosphor-icons\/react(\/|$)/,
35
+ /^@radix-ui\/react-icons$/,
36
+ /^@tabler\/icons-react$/,
37
+ /^react-feather$/,
38
+ /^feather-icons$/,
39
+ /^@fortawesome\//,
40
+ /^@ant-design\/icons(\/|$)/,
41
+ /^react-bootstrap-icons$/,
42
+ /^@primer\/octicons-react$/,
43
+ /^grommet-icons$/,
44
+ ];
45
+
46
+ // A single emoji glyph: a pictographic code point plus its modifiers - ZWJ
47
+ // (\u200d), variation selector (\uFE0F), combining enclosing keycap (\u20E3).
48
+ // Expressed as alternation (not a character class) to avoid combining marks
49
+ // inside `[...]` (oxlint no-misleading-character-class).
50
+ const HAS_EMOJI = /\p{Extended_Pictographic}/u;
51
+ const ALLOWED_GLYPH = /\s|\u200d|\uFE0F|\u20E3|\p{Extended_Pictographic}/gu;
52
+
53
+ /** True when an import source is a disallowed third-party icon library. */
54
+ export function isUnapprovedIconImport(source) {
55
+ if (typeof source !== "string") return false;
56
+ if (APPROVED_SOURCES.some((re) => re.test(source))) return false;
57
+ return ICON_LIBRARIES.some((re) => re.test(source));
58
+ }
59
+
60
+ /** True when a string is only emoji (used as a stand-in for a real icon). */
61
+ export function isEmojiOnly(text) {
62
+ if (typeof text !== "string") return false;
63
+ const trimmed = text.trim();
64
+ if (!trimmed) return false;
65
+ if (!HAS_EMOJI.test(trimmed)) return false;
66
+ // Emoji-only ⇔ nothing remains after removing every emoji glyph + modifier.
67
+ return trimmed.replace(ALLOWED_GLYPH, "") === "";
68
+ }
69
+
70
+ const APPROVED_HINT =
71
+ "Import icons from @flanksource/clicky-ui/icons (Ui* components), " +
72
+ "@flanksource/icons/mi, or @iconify/react";
73
+
74
+ const rule = {
75
+ meta: {
76
+ type: "suggestion",
77
+ docs: {
78
+ description:
79
+ "Import icons from approved sources and use real icon components, not emoji/text labels",
80
+ },
81
+ },
82
+ create(context) {
83
+ return {
84
+ ImportDeclaration(node) {
85
+ if (isUnapprovedIconImport(node.source?.value)) {
86
+ context.report({
87
+ message: `"${node.source.value}" is not an approved icon source. ${APPROVED_HINT}.`,
88
+ node,
89
+ });
90
+ }
91
+ },
92
+ Literal(node) {
93
+ if (isIconifyName(node.value)) {
94
+ context.report({
95
+ message: `Iconify icon name "${node.value}" passed as a string. ${APPROVED_HINT} and pass the component.`,
96
+ node,
97
+ });
98
+ }
99
+ },
100
+ JSXText(node) {
101
+ if (isEmojiOnly(node.value)) {
102
+ context.report({
103
+ message: `Emoji used as an icon. ${APPROVED_HINT}.`,
104
+ node,
105
+ });
106
+ }
107
+ },
108
+ JSXAttribute(node) {
109
+ if (attributeName(node) !== "icon") return;
110
+ if (isEmojiOnly(literalString(node.value))) {
111
+ context.report({
112
+ message: `Emoji passed to \`icon\`. ${APPROVED_HINT} and pass the component.`,
113
+ node,
114
+ });
115
+ }
116
+ },
117
+ };
118
+ },
119
+ };
120
+
121
+ export default rule;
@@ -0,0 +1,98 @@
1
+ // clicky-ui/prefer-tailwind-classes — flag inline `style={{…}}` that hardcodes
2
+ // spacing/layout/typography with static values which have direct Tailwind
3
+ // utilities. Computed values (var()/calc()/clamp(), identifiers, expressions)
4
+ // are intentionally allowed — those legitimately cannot be a static class.
5
+ //
6
+ // Color properties are deliberately NOT handled here; they belong to the theme
7
+ // concern (clicky-ui/prefer-theme-tokens).
8
+
9
+ import { attributeName, propertyKey, styleObject } from "./clicky-ui-shared.js";
10
+
11
+ // CSS properties with a clean Tailwind utility equivalent (non-color).
12
+ const TAILWIND_MAPPABLE = new Set([
13
+ "padding",
14
+ "paddingTop",
15
+ "paddingRight",
16
+ "paddingBottom",
17
+ "paddingLeft",
18
+ "paddingInline",
19
+ "paddingBlock",
20
+ "margin",
21
+ "marginTop",
22
+ "marginRight",
23
+ "marginBottom",
24
+ "marginLeft",
25
+ "marginInline",
26
+ "marginBlock",
27
+ "gap",
28
+ "rowGap",
29
+ "columnGap",
30
+ "display",
31
+ "flexDirection",
32
+ "justifyContent",
33
+ "alignItems",
34
+ "textAlign",
35
+ "fontWeight",
36
+ "fontSize",
37
+ "lineHeight",
38
+ "borderRadius",
39
+ ]);
40
+
41
+ // Dynamic CSS functions whose result can't be a static utility class.
42
+ const DYNAMIC_CSS = /\b(?:var|calc|clamp|min|max|env)\(/;
43
+
44
+ /**
45
+ * True when a style property should instead be a Tailwind class: it is a
46
+ * mappable property AND its value is a static literal (number, or string with
47
+ * no dynamic CSS function). `value` is the already-extracted literal value.
48
+ */
49
+ export function isHandrolledStyleValue(property, value) {
50
+ if (!TAILWIND_MAPPABLE.has(property)) return false;
51
+ if (typeof value === "number") return true;
52
+ if (typeof value !== "string") return false;
53
+ return !DYNAMIC_CSS.test(value);
54
+ }
55
+
56
+ /** Extract a static literal value (string/number) from a Property value node. */
57
+ function staticValue(node) {
58
+ if (node?.type === "Literal" && (typeof node.value === "string" || typeof node.value === "number")) {
59
+ return node.value;
60
+ }
61
+ return undefined;
62
+ }
63
+
64
+ const rule = {
65
+ meta: {
66
+ type: "suggestion",
67
+ docs: {
68
+ description:
69
+ "Use Tailwind utility classes instead of hand-rolling static inline styles",
70
+ },
71
+ },
72
+ create(context) {
73
+ return {
74
+ JSXAttribute(node) {
75
+ if (attributeName(node) !== "style") return;
76
+ const object = styleObject(node);
77
+ if (!object) return;
78
+ for (const prop of object.properties) {
79
+ const key = propertyKey(prop);
80
+ if (!key) continue;
81
+ const value = staticValue(prop.value);
82
+ if (value === undefined) continue;
83
+ if (isHandrolledStyleValue(key, value)) {
84
+ context.report({
85
+ message:
86
+ `Hand-rolled inline style \`${key}\`. Use the equivalent Tailwind ` +
87
+ "class (and density utilities like `p-density-4`/`gap-density-2`) " +
88
+ "so spacing tracks the active density.",
89
+ node: prop,
90
+ });
91
+ }
92
+ }
93
+ },
94
+ };
95
+ },
96
+ };
97
+
98
+ export default rule;
@@ -0,0 +1,134 @@
1
+ // clicky-ui/prefer-theme-tokens — keep consumers on the theme + density system
2
+ // instead of bypassing it. Two anti-patterns:
3
+ // 1. Hardcoded colors (className `text-[#fff]`, `style={{ color: "#fff" }}`)
4
+ // that don't flip with `[data-theme]` — use semantic tokens (text-foreground,
5
+ // bg-background, border-border, …).
6
+ // 2. Re-reading the clicky theme/density localStorage keys by hand instead of
7
+ // the `useTheme`/`useDensity` hooks (or ThemeProvider/DensityProvider).
8
+
9
+ import {
10
+ attributeName,
11
+ classTokens,
12
+ collectStrings,
13
+ literalString,
14
+ propertyKey,
15
+ styleObject,
16
+ } from "./clicky-ui-shared.js";
17
+
18
+ // Arbitrary Tailwind color class with a literal color, e.g. `text-[#fff]`,
19
+ // `bg-[rgb(0,0,0)]`, `hover:border-[#abc]`.
20
+ const ARBITRARY_COLOR_CLASS =
21
+ /(?:^|:)(?:text|bg|border|ring|fill|stroke|from|via|to|outline|caret|decoration|accent|shadow|divide)-\[(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla)\()/;
22
+
23
+ // A literal CSS color value: hex, rgb()/rgba(), hsl()/hsla().
24
+ const HARDCODED_COLOR = /(?:#[0-9a-fA-F]{3,8}\b|\b(?:rgb|rgba|hsl|hsla)\()/;
25
+
26
+ // CSS properties whose hardcoded color should be a theme token instead.
27
+ const COLOR_PROPS = new Set([
28
+ "color",
29
+ "backgroundColor",
30
+ "background",
31
+ "borderColor",
32
+ "outlineColor",
33
+ "caretColor",
34
+ "textDecorationColor",
35
+ "fill",
36
+ "stroke",
37
+ ]);
38
+
39
+ const STORAGE_KEYS = new Set(["clicky-ui-theme", "clicky-ui-density"]);
40
+ const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
41
+
42
+ /** True for a Tailwind class hardcoding a literal color (`text-[#fff]`). */
43
+ export function isArbitraryColorClass(token) {
44
+ return typeof token === "string" && ARBITRARY_COLOR_CLASS.test(token);
45
+ }
46
+
47
+ /** True for a literal CSS color value (hex / rgb / hsl). */
48
+ export function isHardcodedColorValue(value) {
49
+ return typeof value === "string" && HARDCODED_COLOR.test(value);
50
+ }
51
+
52
+ /** True for a clicky-ui theme/density localStorage key. */
53
+ export function isClickyStorageKey(value) {
54
+ return typeof value === "string" && STORAGE_KEYS.has(value);
55
+ }
56
+
57
+ const TOKEN_HINT =
58
+ "use a semantic theme token (text-foreground, bg-background, border-border, …) " +
59
+ "so it tracks light/dark via [data-theme]";
60
+
61
+ function checkClassName(node, context) {
62
+ if (attributeName(node) !== "className" && attributeName(node) !== "class") return;
63
+ for (const token of classTokens(collectStrings(node.value))) {
64
+ if (isArbitraryColorClass(token)) {
65
+ context.report({ message: `Hardcoded color \`${token}\` — ${TOKEN_HINT}.`, node });
66
+ return;
67
+ }
68
+ }
69
+ }
70
+
71
+ function checkStyleColor(node, context) {
72
+ if (attributeName(node) !== "style") return;
73
+ const object = styleObject(node);
74
+ if (!object) return;
75
+ for (const prop of object.properties) {
76
+ const key = propertyKey(prop);
77
+ if (!key || !COLOR_PROPS.has(key)) continue;
78
+ if (isHardcodedColorValue(literalString(prop.value))) {
79
+ context.report({
80
+ message: `Hardcoded \`${key}\` color — ${TOKEN_HINT}.`,
81
+ node: prop,
82
+ });
83
+ }
84
+ }
85
+ }
86
+
87
+ /** `localStorage`, `window.localStorage`, `globalThis.sessionStorage`, … */
88
+ function isStorageObject(node) {
89
+ if (node?.type === "Identifier") return STORAGE_OBJECTS.has(node.name);
90
+ if (node?.type === "MemberExpression" && node.property?.type === "Identifier") {
91
+ return STORAGE_OBJECTS.has(node.property.name);
92
+ }
93
+ return false;
94
+ }
95
+
96
+ function checkStorageCall(node, context) {
97
+ const callee = node.callee;
98
+ if (callee?.type !== "MemberExpression") return;
99
+ if (!isStorageObject(callee.object)) return;
100
+ for (const arg of node.arguments) {
101
+ if (isClickyStorageKey(literalString(arg))) {
102
+ context.report({
103
+ message:
104
+ `Reading "${arg.value}" from storage by hand. Use clicky-ui's useTheme()/` +
105
+ "useDensity() hooks (or ThemeProvider/DensityProvider) instead.",
106
+ node,
107
+ });
108
+ return;
109
+ }
110
+ }
111
+ }
112
+
113
+ const rule = {
114
+ meta: {
115
+ type: "problem",
116
+ docs: {
117
+ description:
118
+ "Use clicky-ui theme tokens and theme/density hooks instead of hardcoded colors or raw storage access",
119
+ },
120
+ },
121
+ create(context) {
122
+ return {
123
+ JSXAttribute(node) {
124
+ checkClassName(node, context);
125
+ checkStyleColor(node, context);
126
+ },
127
+ CallExpression(node) {
128
+ checkStorageCall(node, context);
129
+ },
130
+ };
131
+ },
132
+ };
133
+
134
+ export default rule;