@kennycorrea/ui 0.1.0 → 0.2.0

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 (67) hide show
  1. package/README.md +96 -10
  2. package/dist/atoms/Avatar/Avatar.js +1 -1
  3. package/dist/atoms/Avatar/index.d.ts +1 -1
  4. package/dist/atoms/Avatar/index.js +1 -1
  5. package/dist/atoms/Badge/Badge.js +1 -1
  6. package/dist/atoms/Badge/index.d.ts +1 -1
  7. package/dist/atoms/Badge/index.js +1 -1
  8. package/dist/atoms/Button/Button.d.ts +1 -1
  9. package/dist/atoms/Button/Button.js +2 -2
  10. package/dist/atoms/Button/Button.types.d.ts +1 -1
  11. package/dist/atoms/Button/index.d.ts +2 -2
  12. package/dist/atoms/Button/index.js +2 -2
  13. package/dist/atoms/Card/Card.d.ts +1 -1
  14. package/dist/atoms/Card/Card.js +2 -2
  15. package/dist/atoms/Card/index.d.ts +1 -1
  16. package/dist/atoms/Card/index.js +1 -1
  17. package/dist/atoms/Checkbox/Checkbox.js +1 -1
  18. package/dist/atoms/Checkbox/index.d.ts +1 -1
  19. package/dist/atoms/Checkbox/index.js +1 -1
  20. package/dist/atoms/ContextMenu/index.d.ts +1 -1
  21. package/dist/atoms/ContextMenu/index.js +1 -1
  22. package/dist/atoms/Dialog/Dialog.d.ts +1 -1
  23. package/dist/atoms/Dialog/Dialog.js +2 -2
  24. package/dist/atoms/Dialog/index.d.ts +1 -1
  25. package/dist/atoms/Dialog/index.js +1 -1
  26. package/dist/atoms/Icon/Icon.js +1 -1
  27. package/dist/atoms/Icon/index.d.ts +1 -1
  28. package/dist/atoms/Icon/index.js +1 -1
  29. package/dist/atoms/Input/Input.js +1 -1
  30. package/dist/atoms/Input/index.d.ts +1 -1
  31. package/dist/atoms/Input/index.js +1 -1
  32. package/dist/atoms/Label/Label.js +1 -1
  33. package/dist/atoms/Label/index.d.ts +1 -1
  34. package/dist/atoms/Label/index.js +1 -1
  35. package/dist/atoms/Popover/Popover.js +1 -1
  36. package/dist/atoms/Popover/index.d.ts +1 -1
  37. package/dist/atoms/Popover/index.js +1 -1
  38. package/dist/atoms/Progress/Progress.js +1 -1
  39. package/dist/atoms/Progress/index.d.ts +1 -1
  40. package/dist/atoms/Progress/index.js +1 -1
  41. package/dist/atoms/Select/Select.js +2 -2
  42. package/dist/atoms/Select/index.d.ts +1 -1
  43. package/dist/atoms/Select/index.js +1 -1
  44. package/dist/atoms/Separator/Separator.js +1 -1
  45. package/dist/atoms/Separator/index.d.ts +1 -1
  46. package/dist/atoms/Separator/index.js +1 -1
  47. package/dist/atoms/Spinner/Spinner.js +2 -2
  48. package/dist/atoms/Spinner/index.d.ts +1 -1
  49. package/dist/atoms/Spinner/index.js +1 -1
  50. package/dist/atoms/Switch/Switch.js +1 -1
  51. package/dist/atoms/Switch/index.d.ts +1 -1
  52. package/dist/atoms/Switch/index.js +1 -1
  53. package/dist/atoms/Textarea/Textarea.js +1 -1
  54. package/dist/atoms/Textarea/index.d.ts +1 -1
  55. package/dist/atoms/Textarea/index.js +1 -1
  56. package/dist/atoms/Tooltip/index.d.ts +1 -1
  57. package/dist/atoms/Tooltip/index.js +1 -1
  58. package/dist/atoms/index.d.ts +18 -18
  59. package/dist/atoms/index.js +18 -18
  60. package/dist/index.d.ts +3 -3
  61. package/dist/index.js +3 -3
  62. package/dist/utils/index.d.ts +2 -2
  63. package/dist/utils/index.js +2 -2
  64. package/dist/utils/theme.d.ts +81 -13
  65. package/dist/utils/theme.js +110 -47
  66. package/package.json +1 -1
  67. package/tokens.css +3 -3
package/README.md CHANGED
@@ -63,6 +63,58 @@ And in the app's own `index.css`:
63
63
  every utility class used *inside* the atoms — `w-full`, `inline-flex` — is missing from the
64
64
  generated CSS. The components come out subtly unstyled and nothing appears in the console.
65
65
 
66
+ ## Theming — 0.2.0
67
+
68
+ **A product configures its own accents, default and storage namespace.** Until 0.2.0 all three
69
+ were hardcoded to the first app that used this package: the fallback accent was jade, the accent
70
+ list was six fixed colours, and preferences were stored under `upfront.theme` / `upfront.accent`.
71
+
72
+ ```ts
73
+ // Once, in the app.
74
+ export const theming = createTheming({
75
+ accents: ['teal', 'crimson'],
76
+ defaultAccent: 'teal',
77
+ storagePrefix: 'myapp',
78
+ });
79
+
80
+ theming.readAccent(); // 'teal' | 'crimson'
81
+ ```
82
+
83
+ Any Radix accent name works, and a product with one brand colour passes one. The list is the
84
+ order a picker should render.
85
+
86
+ **`defaultAccent` is typed as a member of `accents`**, so naming one that is not on the list is
87
+ `TS2322` rather than a colourless first paint — verified by compiling the mistake.
88
+
89
+ **The literal array needs no `as const`**; the `const` type parameter infers the tuple. A list
90
+ declared separately does, because `const list = ['teal']` widens to `string[]` and is rejected.
91
+
92
+ ⚠ **`storagePrefix` is required and has no default.** A default would be one product's name
93
+ silently storing another's preferences, and because separate origins never collide, nothing would
94
+ ever reveal it.
95
+
96
+ ⚠ **Every accent needs its Radix colour scale imported** in the app's `main.tsx` — see Colour
97
+ scales below. A missing scale is not an error; it is a colourless app.
98
+
99
+ **A factory, not a `configure()` call.** Module-level mutable state would have to be written
100
+ before the first read, making correctness depend on import order — the same class of silent bug as
101
+ the CSS import order this package already has to warn about. An instance cannot be read before it
102
+ is made.
103
+
104
+ `applyTheme` stays a free function, because `data-theme` is the contract between it and
105
+ `tokens.css` rather than a preference: two apps disagreeing about the attribute name would each
106
+ need their own stylesheet.
107
+
108
+ ### What still works unchanged
109
+
110
+ `readTheme`, `storeTheme`, `readAccent` and `storeAccent` are still exported as free functions,
111
+ bound to an instance with the six default accents, jade, and the `upfront.*` keys. **One
112
+ implementation, not two** — they are literally a pre-made instance, so no call site had to change
113
+ and nobody who already picked a theme loses it. `ACCENTS` remains as a deprecated alias for
114
+ `DEFAULT_ACCENTS`, the same array.
115
+
116
+ A new product should call `createTheming` with its own name rather than inherit those.
117
+
66
118
  ## What is deliberately not here
67
119
 
68
120
  - **Dictionaries.** Words belong to an app. The two strings the atoms say on their own behalf —
@@ -83,6 +135,29 @@ load-bearing** — scales define `--gray-*` literally and `base.css` remaps it o
83
135
  `grayColor`, so `base.css` must come last. Adding an accent to `ACCENTS` means adding its scale
84
136
  in every app that offers it, or it resolves to nothing.
85
137
 
138
+ ## ⚠ 0.1.0 was broken for any Node consumer
139
+
140
+ **Fixed in 0.1.1.** The package compiled with `moduleResolution: bundler`, which leaves relative
141
+ imports extensionless in the output — `export * from './atoms'`. Vite resolves that happily, so
142
+ every build passed and the linked `file:../ui` setup never complained. **Node does not support
143
+ directory imports**, so the moment the package was installed from the registry it failed:
144
+
145
+ Directory import '…/@kennycorrea/ui/dist/atoms' is not supported
146
+ resolving ES modules imported from …/@kennycorrea/ui/dist/index.js
147
+
148
+ It surfaced in the dashboard's Vitest run, which resolves dependencies with Node semantics. A
149
+ bundler-only consumer would never have noticed, which is why it shipped.
150
+
151
+ The fix is the rule this project already applies in `api/`, whose own brief states it: **an
152
+ explicit `.js` on every relative import**, and `/index.js` for a directory. `tsconfig.json` is now
153
+ `moduleResolution: nodenext`, so `tsc` *refuses* the extensionless form — verified by putting one
154
+ back and watching it error — rather than silently emitting it again.
155
+
156
+ ⚠ **The lesson is about what "the build passes" is worth.** Three apps built, 46 tests passed and
157
+ a browser check confirmed the styling, all while the package was unimportable by Node. Nothing was
158
+ wrong with those checks; they simply all went through the same bundler. The first consumer with
159
+ different resolution semantics found it immediately.
160
+
86
161
  ## Releasing
87
162
 
88
163
  `npm run build` compiles to `dist/` with type declarations. The version in `package.json` is the
@@ -137,17 +212,28 @@ That was the state when the workflow was first added.
137
212
 
138
213
  ### Where it is consumed from today
139
214
 
140
- ⚠ **The three apps still depend on `file:../ui`**, which works while they sit side by side on one
141
- disk and does **not** work on Vercel, where each project builds from its own repository.
142
-
143
- Once this is published, each consumer's dependency becomes a version instead of a path:
215
+ ✅ **All three apps depend on a version from the registry**, as of 2026-09-30. The
216
+ `file:../ui` arrangement is gone, and each app now builds on Vercel from its own repository.
144
217
 
145
218
  ```jsonc
146
- // web, site and admin package.json
147
- "@kennycorrea/ui": "^0.1.0" // instead of "file:../ui"
219
+ // a new consumer
220
+ "@kennycorrea/ui": "^0.2.0"
221
+
222
+ // web, site and admin, still — see the note below
223
+ "@kennycorrea/ui": "^0.1.1"
148
224
  ```
149
225
 
150
- That is one line in three files and no code change. Note that `file:` installs a **symlink**, so
151
- local development currently picks up edits to this package immediately; a published version will
152
- not, and an app wanting to test an unreleased change wants `npm link` or a `file:` override
153
- rather than a copy.
226
+ ⚠ **Never floor a range at 0.1.0** — its published output cannot be imported by Node. `^0.1.0`
227
+ resolves correctly today because npm picks the newest match, but a range whose floor is unusable
228
+ documents nothing and would let a lockfile built from scratch land back on it.
229
+
230
+ ⚠ **Under `0.x`, `^` does not cross a minor.** `^0.1.1` will **not** take 0.2.0, so moving a
231
+ consumer onto the new theming API is a `package.json` change in each app, not just a lockfile
232
+ refresh. The free functions still work, so an app that does not need `createTheming` can stay
233
+ where it is — and the three existing apps deliberately do, for now. Moving them is a one-line
234
+ change each plus a lockfile, worth doing to stop three consumers drifting a minor behind, and
235
+ worth doing as its own commit rather than inside this one.
236
+
237
+ ⚠ **A `file:` install was a symlink; a version install is a copy.** Edits here no longer appear in
238
+ a consumer. An app that needs an unreleased change wants `npm link` or a temporary `file:`
239
+ override — never a copied file, which is the drift this package exists to end.
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Avatar as ThemedAvatar } from '@radix-ui/themes';
3
- import { cn } from '../../utils/cn';
3
+ import { cn } from '../../utils/cn.js';
4
4
  /** Two letters from the name, which is all a small circle has room for. */
5
5
  function initialsOf(name) {
6
6
  if (!name)
@@ -1 +1 @@
1
- export * from './Avatar';
1
+ export * from './Avatar.js';
@@ -1 +1 @@
1
- export * from './Avatar';
1
+ export * from './Avatar.js';
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Badge as ThemedBadge } from '@radix-ui/themes';
3
3
  /**
4
- * Upfront's five meanings, in Themes' colour vocabulary.
4
+ * The five meanings this library names, in Themes' colour vocabulary.
5
5
  *
6
6
  * **`brand` names no colour on purpose.** Themes falls back to whatever accent the theme
7
7
  * carries, so a badge that means "this is ours" follows the business's chosen colour while
@@ -1 +1 @@
1
- export * from './Badge';
1
+ export * from './Badge.js';
@@ -1 +1 @@
1
- export * from './Badge';
1
+ export * from './Badge.js';
@@ -1,2 +1,2 @@
1
- import type { ButtonProps } from './Button.types';
1
+ import type { ButtonProps } from './Button.types.js';
2
2
  export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Button as ThemedButton } from '@radix-ui/themes';
3
3
  import { forwardRef } from 'react';
4
- import { cn } from '../../utils/cn';
4
+ import { cn } from '../../utils/cn.js';
5
5
  /**
6
- * Upfront's intents, expressed in Radix Themes' variant-and-colour vocabulary.
6
+ * The house intents, expressed in Radix Themes' variant-and-colour vocabulary.
7
7
  *
8
8
  * The mapping exists so three apps keep saying `variant="danger"` rather than
9
9
  * `variant="solid" color="red"` at sixty call sites — and so that when the house style changes
@@ -1,7 +1,7 @@
1
1
  import type { Button as ThemedButton } from '@radix-ui/themes';
2
2
  import type { ComponentPropsWithoutRef, ReactNode } from 'react';
3
3
  /**
4
- * Upfront's button intents.
4
+ * The house button intents.
5
5
  *
6
6
  * **`outline` is new, and it is how the disagreement was resolved.** The dashboard's
7
7
  * `secondary` mapped to Themes' `soft` and the marketing site's mapped to `surface` — the same
@@ -1,2 +1,2 @@
1
- export * from './Button';
2
- export * from './Button.types';
1
+ export * from './Button.js';
2
+ export * from './Button.types.js';
@@ -1,2 +1,2 @@
1
- export * from './Button';
2
- export * from './Button.types';
1
+ export * from './Button.js';
2
+ export * from './Button.types.js';
@@ -6,7 +6,7 @@ export interface CardProps extends HTMLAttributes<HTMLDivElement> {
6
6
  /**
7
7
  * Radix Themes' Card, with the padding taken off.
8
8
  *
9
- * Themes pads its card for you; Upfront's cards are built from a header and a body with a
9
+ * Themes pads its card for you; the house card is built from a header and a body with a
10
10
  * rule between them, so the padding belongs to those rather than to the card. `p-0` is the
11
11
  * whole of the customisation.
12
12
  */
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Card as ThemedCard } from '@radix-ui/themes';
3
- import { cn } from '../../utils/cn';
3
+ import { cn } from '../../utils/cn.js';
4
4
  /**
5
5
  * Radix Themes' Card, with the padding taken off.
6
6
  *
7
- * Themes pads its card for you; Upfront's cards are built from a header and a body with a
7
+ * Themes pads its card for you; the house card is built from a header and a body with a
8
8
  * rule between them, so the padding belongs to those rather than to the card. `p-0` is the
9
9
  * whole of the customisation.
10
10
  */
@@ -1 +1 @@
1
- export * from './Card';
1
+ export * from './Card.js';
@@ -1 +1 @@
1
- export * from './Card';
1
+ export * from './Card.js';
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Checkbox as ThemedCheckbox } from '@radix-ui/themes';
3
3
  import { forwardRef } from 'react';
4
- import { cn } from '../../utils/cn';
4
+ import { cn } from '../../utils/cn.js';
5
5
  /**
6
6
  * A checkbox, for a list where several answers are true at once.
7
7
  *
@@ -1 +1 @@
1
- export * from './Checkbox';
1
+ export * from './Checkbox.js';
@@ -1 +1 @@
1
- export * from './Checkbox';
1
+ export * from './Checkbox.js';
@@ -1 +1 @@
1
- export * from './ContextMenu';
1
+ export * from './ContextMenu.js';
@@ -1 +1 @@
1
- export * from './ContextMenu';
1
+ export * from './ContextMenu.js';
@@ -10,7 +10,7 @@ export interface DialogProps {
10
10
  className?: string;
11
11
  }
12
12
  /**
13
- * Themes' dialog, laid out the way Upfront's are: a titled header, a body, and a rule above
13
+ * Themes' dialog, laid out the way the house ones are: a titled header, a body, and a rule above
14
14
  * the actions.
15
15
  *
16
16
  * The padding is taken off the content so those three bands can own it — Themes pads the
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Dialog as ThemedDialog } from '@radix-ui/themes';
3
- import { cn } from '../../utils/cn';
3
+ import { cn } from '../../utils/cn.js';
4
4
  /**
5
- * Themes' dialog, laid out the way Upfront's are: a titled header, a body, and a rule above
5
+ * Themes' dialog, laid out the way the house ones are: a titled header, a body, and a rule above
6
6
  * the actions.
7
7
  *
8
8
  * The padding is taken off the content so those three bands can own it — Themes pads the
@@ -1 +1 @@
1
- export * from './Dialog';
1
+ export * from './Dialog.js';
@@ -1 +1 @@
1
- export * from './Dialog';
1
+ export * from './Dialog.js';
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { cn } from '../../utils/cn';
2
+ import { cn } from '../../utils/cn.js';
3
3
  /**
4
4
  * The handful of icons this app actually uses, drawn inline.
5
5
  *
@@ -1 +1 @@
1
- export * from './Icon';
1
+ export * from './Icon.js';
@@ -1 +1 @@
1
- export * from './Icon';
1
+ export * from './Icon.js';
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { TextField } from '@radix-ui/themes';
3
3
  import { forwardRef } from 'react';
4
- import { cn } from '../../utils/cn';
4
+ import { cn } from '../../utils/cn.js';
5
5
  const SIZES = { sm: '1', md: '2', lg: '3' };
6
6
  export const Input = forwardRef(function Input({ size = 'md', invalid = false, className, ...props }, ref) {
7
7
  return (_jsx(TextField.Root, { ref: ref, size: SIZES[size],
@@ -1 +1 @@
1
- export * from './Input';
1
+ export * from './Input.js';
@@ -1 +1 @@
1
- export * from './Input';
1
+ export * from './Input.js';
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Text } from '@radix-ui/themes';
3
3
  import { forwardRef } from 'react';
4
- import { cn } from '../../utils/cn';
4
+ import { cn } from '../../utils/cn.js';
5
5
  /**
6
6
  * Themes' `Text` rendered as a label, so field labels sit on the same type scale as
7
7
  * everything else.
@@ -1 +1 @@
1
- export * from './Label';
1
+ export * from './Label.js';
@@ -1 +1 @@
1
- export * from './Label';
1
+ export * from './Label.js';
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Popover as ThemedPopover } from '@radix-ui/themes';
3
- import { cn } from '../../utils/cn';
3
+ import { cn } from '../../utils/cn.js';
4
4
  /**
5
5
  * A popover rather than a dropdown menu: the contents are things to read — the bell feed —
6
6
  * not commands to run, and a menu's roving focus would make arrow keys skip past text
@@ -1 +1 @@
1
- export * from './Popover';
1
+ export * from './Popover.js';
@@ -1 +1 @@
1
- export * from './Popover';
1
+ export * from './Popover.js';
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Progress as ThemedProgress } from '@radix-ui/themes';
3
- import { cn } from '../../utils/cn';
3
+ import { cn } from '../../utils/cn.js';
4
4
  export function Progress({ value, label, color = 'jade', className }) {
5
5
  return (_jsx(ThemedProgress
6
6
  // Rounded for the announcement, clamped for both: turnover can genuinely pass its
@@ -1 +1 @@
1
- export * from './Progress';
1
+ export * from './Progress.js';
@@ -1 +1 @@
1
- export * from './Progress';
1
+ export * from './Progress.js';
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Select as ThemedSelect } from '@radix-ui/themes';
3
3
  import { forwardRef } from 'react';
4
- import { useUiStrings } from '../../strings';
5
- import { cn } from '../../utils/cn';
4
+ import { useUiStrings } from '../../strings.js';
5
+ import { cn } from '../../utils/cn.js';
6
6
  /**
7
7
  * Themes' Select, which is its own styling of the same Radix primitive this used to wrap by
8
8
  * hand — the chevron, the popper, the checked item and the scroll buttons all come with it.
@@ -1 +1 @@
1
- export * from './Select';
1
+ export * from './Select.js';
@@ -1 +1 @@
1
- export * from './Select';
1
+ export * from './Select.js';
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Separator as ThemedSeparator } from '@radix-ui/themes';
3
- import { cn } from '../../utils/cn';
3
+ import { cn } from '../../utils/cn.js';
4
4
  export function Separator({ orientation = 'horizontal', className }) {
5
5
  return _jsx(ThemedSeparator, { orientation: orientation, size: "4", className: cn(className) });
6
6
  }
@@ -1 +1 @@
1
- export * from './Separator';
1
+ export * from './Separator.js';
@@ -1 +1 @@
1
- export * from './Separator';
1
+ export * from './Separator.js';
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Spinner as ThemedSpinner } from '@radix-ui/themes';
3
- import { useUiStrings } from '../../strings';
4
- import { cn } from '../../utils/cn';
3
+ import { useUiStrings } from '../../strings.js';
4
+ import { cn } from '../../utils/cn.js';
5
5
  /**
6
6
  * Themes' spinner, wrapped so it still announces itself.
7
7
  *
@@ -1 +1 @@
1
- export * from './Spinner';
1
+ export * from './Spinner.js';
@@ -1 +1 @@
1
- export * from './Spinner';
1
+ export * from './Spinner.js';
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Switch as ThemedSwitch } from '@radix-ui/themes';
3
3
  import { forwardRef } from 'react';
4
- import { cn } from '../../utils/cn';
4
+ import { cn } from '../../utils/cn.js';
5
5
  export const Switch = forwardRef(function Switch({ className, ...props }, ref) {
6
6
  return _jsx(ThemedSwitch, { ref: ref, size: "2", className: cn(className), ...props });
7
7
  });
@@ -1 +1 @@
1
- export * from './Switch';
1
+ export * from './Switch.js';
@@ -1 +1 @@
1
- export * from './Switch';
1
+ export * from './Switch.js';
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { TextArea } from '@radix-ui/themes';
3
3
  import { forwardRef } from 'react';
4
- import { cn } from '../../utils/cn';
4
+ import { cn } from '../../utils/cn.js';
5
5
  export const Textarea = forwardRef(function Textarea({ invalid = false, className, rows = 3, ...props }, ref) {
6
6
  return (_jsx(TextArea, { ref: ref, rows: rows, size: "2", color: invalid ? 'red' : undefined, "aria-invalid": invalid || undefined, className: cn('w-full', className), ...props }));
7
7
  });
@@ -1 +1 @@
1
- export * from './Textarea';
1
+ export * from './Textarea.js';
@@ -1 +1 @@
1
- export * from './Textarea';
1
+ export * from './Textarea.js';
@@ -1 +1 @@
1
- export * from './Tooltip';
1
+ export * from './Tooltip.js';
@@ -1 +1 @@
1
- export * from './Tooltip';
1
+ export * from './Tooltip.js';
@@ -1,18 +1,18 @@
1
- export * from './Avatar';
2
- export * from './Badge';
3
- export * from './Button';
4
- export * from './Card';
5
- export * from './Checkbox';
6
- export * from './ContextMenu';
7
- export * from './Dialog';
8
- export * from './Icon';
9
- export * from './Input';
10
- export * from './Label';
11
- export * from './Popover';
12
- export * from './Progress';
13
- export * from './Select';
14
- export * from './Separator';
15
- export * from './Spinner';
16
- export * from './Switch';
17
- export * from './Textarea';
18
- export * from './Tooltip';
1
+ export * from './Avatar/index.js';
2
+ export * from './Badge/index.js';
3
+ export * from './Button/index.js';
4
+ export * from './Card/index.js';
5
+ export * from './Checkbox/index.js';
6
+ export * from './ContextMenu/index.js';
7
+ export * from './Dialog/index.js';
8
+ export * from './Icon/index.js';
9
+ export * from './Input/index.js';
10
+ export * from './Label/index.js';
11
+ export * from './Popover/index.js';
12
+ export * from './Progress/index.js';
13
+ export * from './Select/index.js';
14
+ export * from './Separator/index.js';
15
+ export * from './Spinner/index.js';
16
+ export * from './Switch/index.js';
17
+ export * from './Textarea/index.js';
18
+ export * from './Tooltip/index.js';
@@ -1,18 +1,18 @@
1
- export * from './Avatar';
2
- export * from './Badge';
3
- export * from './Button';
4
- export * from './Card';
5
- export * from './Checkbox';
6
- export * from './ContextMenu';
7
- export * from './Dialog';
8
- export * from './Icon';
9
- export * from './Input';
10
- export * from './Label';
11
- export * from './Popover';
12
- export * from './Progress';
13
- export * from './Select';
14
- export * from './Separator';
15
- export * from './Spinner';
16
- export * from './Switch';
17
- export * from './Textarea';
18
- export * from './Tooltip';
1
+ export * from './Avatar/index.js';
2
+ export * from './Badge/index.js';
3
+ export * from './Button/index.js';
4
+ export * from './Card/index.js';
5
+ export * from './Checkbox/index.js';
6
+ export * from './ContextMenu/index.js';
7
+ export * from './Dialog/index.js';
8
+ export * from './Icon/index.js';
9
+ export * from './Input/index.js';
10
+ export * from './Label/index.js';
11
+ export * from './Popover/index.js';
12
+ export * from './Progress/index.js';
13
+ export * from './Select/index.js';
14
+ export * from './Separator/index.js';
15
+ export * from './Spinner/index.js';
16
+ export * from './Switch/index.js';
17
+ export * from './Textarea/index.js';
18
+ export * from './Tooltip/index.js';
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export * from './atoms';
2
- export * from './strings';
3
- export * from './utils';
1
+ export * from './atoms/index.js';
2
+ export * from './strings.js';
3
+ export * from './utils/index.js';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export * from './atoms';
2
- export * from './strings';
3
- export * from './utils';
1
+ export * from './atoms/index.js';
2
+ export * from './strings.js';
3
+ export * from './utils/index.js';
@@ -1,2 +1,2 @@
1
- export * from './cn';
2
- export * from './theme';
1
+ export * from './cn.js';
2
+ export * from './theme.js';
@@ -1,2 +1,2 @@
1
- export * from './cn';
2
- export * from './theme';
1
+ export * from './cn.js';
2
+ export * from './theme.js';
@@ -7,27 +7,95 @@ export type Theme = (typeof THEMES)[number];
7
7
  * "System" deliberately stamps *nothing*: the CSS falls through to `prefers-color-scheme`
8
8
  * on its own, so following the operating system needs no listener here and keeps working
9
9
  * when the user changes it mid-session.
10
+ *
11
+ * **Outside `createTheming` on purpose.** `data-theme` is the contract between this function
12
+ * and `tokens.css`, not a preference an app gets to set — two apps disagreeing about the
13
+ * attribute name would each need their own stylesheet, which is the thing this package exists
14
+ * to prevent.
10
15
  */
11
16
  export declare function applyTheme(theme: Theme): void;
12
- export declare function readTheme(): Theme;
13
- export declare function storeTheme(theme: Theme): void;
14
17
  /**
15
- * The accents a business can dress an app in.
18
+ * A reasonable set of accents for an app that wants a picker and has no opinion.
16
19
  *
17
20
  * Six of Radix's twenty-six, not all of them: every one costs a colour scale in the bundle —
18
- * see the note in each app's `main.tsx` — and a page of near-identical greens is a worse
19
- * choice than six that are obviously different from across a room. `jade` is first because it
20
- * is Upfront's own, and the default for anyone who never opens the picker.
21
+ * see the `main.tsx` note in the README — and a page of near-identical greens is a worse
22
+ * choice than six that are obviously different from across a room.
21
23
  *
22
- * **Only the accent moves.** Success stays jade, warning amber, danger red — those are
23
- * meanings rather than decoration, and a business whose "paid" badge turned orange because
24
- * they liked orange would have lost something. It does mean picking red or jade makes the
25
- * accent and one semantic colour look alike, which is a fair trade for letting people choose.
24
+ * **It is a default, not the list.** Pass your own to `createTheming`; any Radix accent name
25
+ * works, and a product with one brand colour should pass just that one.
26
26
  *
27
27
  * **The labels are not here.** They are words, and words belong to an app's own dictionary —
28
28
  * the same rule that keeps `UiStrings` down to two entries.
29
29
  */
30
+ export declare const DEFAULT_ACCENTS: readonly ["jade", "blue", "purple", "red", "orange", "yellow"];
31
+ /**
32
+ * @deprecated Prefer `DEFAULT_ACCENTS`, or your own list passed to `createTheming`. Kept
33
+ * because the old name is imported by name in existing apps; it is the same array.
34
+ */
30
35
  export declare const ACCENTS: readonly ["jade", "blue", "purple", "red", "orange", "yellow"];
31
- export type Accent = (typeof ACCENTS)[number];
32
- export declare function readAccent(): Accent;
33
- export declare function storeAccent(accent: Accent): void;
36
+ /** The accents `DEFAULT_ACCENTS` offers. An app with its own list gets its own union. */
37
+ export type Accent = (typeof DEFAULT_ACCENTS)[number];
38
+ export interface ThemingConfig<A extends readonly string[]> {
39
+ /**
40
+ * The accents this product offers, in the order a picker should show them.
41
+ *
42
+ * ⚠ **Each one needs its Radix colour scale imported in the app's `main.tsx`**, or it
43
+ * resolves to nothing — a missing scale is not an error, it is a colourless app.
44
+ */
45
+ accents: A;
46
+ /** The accent for anyone who has never opened the picker. Must be one of `accents`. */
47
+ defaultAccent: A[number];
48
+ /**
49
+ * Namespace for this product's `localStorage` keys, giving `<prefix>.theme` and
50
+ * `<prefix>.accent`.
51
+ *
52
+ * **Required, with no default, deliberately.** A default here would be some other
53
+ * product's name silently storing this one's preferences — and because it would work
54
+ * perfectly on a separate origin, nothing would ever reveal it.
55
+ */
56
+ storagePrefix: string;
57
+ }
58
+ export interface Theming<A extends readonly string[]> {
59
+ /** The configured list, so a picker can render it without being handed it twice. */
60
+ readonly accents: A;
61
+ readonly defaultAccent: A[number];
62
+ readTheme(): Theme;
63
+ storeTheme(theme: Theme): void;
64
+ readAccent(): A[number];
65
+ storeAccent(accent: A[number]): void;
66
+ }
67
+ /**
68
+ * Binds the theme and accent helpers to one product's accents, default and storage namespace.
69
+ *
70
+ * Three things used to be hardcoded here — the fallback accent, the list of accents, and the
71
+ * `localStorage` prefix — and all three were the same mistake: a shared package holding one
72
+ * product's identity. They move together rather than one at a time, because a product that can
73
+ * choose its default and not its palette has not really chosen anything.
74
+ *
75
+ * **A factory rather than a `configure()` call.** Module-level mutable state would have to be
76
+ * written before the first read, which makes correctness depend on import order — the same
77
+ * class of bug as this library's load-bearing CSS import order, and just as silent. An instance
78
+ * cannot be read before it is made.
79
+ *
80
+ * ```ts
81
+ * export const theming = createTheming({
82
+ * accents: ['teal', 'crimson'],
83
+ * defaultAccent: 'teal',
84
+ * storagePrefix: 'myapp',
85
+ * });
86
+ *
87
+ * theming.readAccent(); // 'teal' | 'crimson'
88
+ * ```
89
+ *
90
+ * `defaultAccent` is typed as a member of `accents`, so naming one that is not on the list is a
91
+ * compile error (`TS2322`) rather than a colourless first paint.
92
+ *
93
+ * The literal array needs no `as const` — the `const` type parameter infers the tuple. A list
94
+ * declared separately *does*, because `const list = ['teal']` widens to `string[]`, which is not
95
+ * a tuple and will be rejected: either inline it or write `as const` there.
96
+ */
97
+ export declare function createTheming<const A extends readonly [string, ...string[]]>(config: ThemingConfig<A>): Theming<A>;
98
+ export declare const readTheme: () => Theme;
99
+ export declare const storeTheme: (theme: Theme) => void;
100
+ export declare const readAccent: () => "jade" | "red" | "yellow" | "orange" | "purple" | "blue";
101
+ export declare const storeAccent: (accent: "jade" | "red" | "yellow" | "orange" | "purple" | "blue") => void;
@@ -1,5 +1,4 @@
1
1
  export const THEMES = ['system', 'light', 'dark'];
2
- const STORAGE_KEY = 'upfront.theme';
3
2
  /**
4
3
  * Applies a theme by stamping the root element, or clearing it for "system".
5
4
  *
@@ -7,6 +6,11 @@ const STORAGE_KEY = 'upfront.theme';
7
6
  * "System" deliberately stamps *nothing*: the CSS falls through to `prefers-color-scheme`
8
7
  * on its own, so following the operating system needs no listener here and keeps working
9
8
  * when the user changes it mid-session.
9
+ *
10
+ * **Outside `createTheming` on purpose.** `data-theme` is the contract between this function
11
+ * and `tokens.css`, not a preference an app gets to set — two apps disagreeing about the
12
+ * attribute name would each need their own stylesheet, which is the thing this package exists
13
+ * to prevent.
10
14
  */
11
15
  export function applyTheme(theme) {
12
16
  const root = document.documentElement;
@@ -15,58 +19,117 @@ export function applyTheme(theme) {
15
19
  else
16
20
  root.setAttribute('data-theme', theme);
17
21
  }
18
- export function readTheme() {
19
- try {
20
- const stored = localStorage.getItem(STORAGE_KEY);
21
- return THEMES.includes(stored) ? stored : 'system';
22
- }
23
- catch {
24
- // Private windows and blocked site data both throw. A theme preference is not worth a
25
- // crash, so the system default stands.
26
- return 'system';
27
- }
28
- }
29
- export function storeTheme(theme) {
30
- try {
31
- localStorage.setItem(STORAGE_KEY, theme);
32
- }
33
- catch {
34
- // See readTheme. It applies for this session either way.
35
- }
36
- }
37
22
  /**
38
- * The accents a business can dress an app in.
23
+ * A reasonable set of accents for an app that wants a picker and has no opinion.
39
24
  *
40
25
  * Six of Radix's twenty-six, not all of them: every one costs a colour scale in the bundle —
41
- * see the note in each app's `main.tsx` — and a page of near-identical greens is a worse
42
- * choice than six that are obviously different from across a room. `jade` is first because it
43
- * is Upfront's own, and the default for anyone who never opens the picker.
26
+ * see the `main.tsx` note in the README — and a page of near-identical greens is a worse
27
+ * choice than six that are obviously different from across a room.
44
28
  *
45
- * **Only the accent moves.** Success stays jade, warning amber, danger red — those are
46
- * meanings rather than decoration, and a business whose "paid" badge turned orange because
47
- * they liked orange would have lost something. It does mean picking red or jade makes the
48
- * accent and one semantic colour look alike, which is a fair trade for letting people choose.
29
+ * **It is a default, not the list.** Pass your own to `createTheming`; any Radix accent name
30
+ * works, and a product with one brand colour should pass just that one.
49
31
  *
50
32
  * **The labels are not here.** They are words, and words belong to an app's own dictionary —
51
33
  * the same rule that keeps `UiStrings` down to two entries.
52
34
  */
53
- export const ACCENTS = ['jade', 'blue', 'purple', 'red', 'orange', 'yellow'];
54
- const ACCENT_KEY = 'upfront.accent';
55
- export function readAccent() {
56
- try {
57
- const stored = localStorage.getItem(ACCENT_KEY);
58
- return ACCENTS.includes(stored) ? stored : 'jade';
59
- }
60
- catch {
61
- // Same as readTheme: a private window throws, and a colour is not worth a crash.
62
- return 'jade';
63
- }
64
- }
65
- export function storeAccent(accent) {
66
- try {
67
- localStorage.setItem(ACCENT_KEY, accent);
68
- }
69
- catch {
70
- // It applies for this session either way.
71
- }
35
+ export const DEFAULT_ACCENTS = ['jade', 'blue', 'purple', 'red', 'orange', 'yellow'];
36
+ /**
37
+ * @deprecated Prefer `DEFAULT_ACCENTS`, or your own list passed to `createTheming`. Kept
38
+ * because the old name is imported by name in existing apps; it is the same array.
39
+ */
40
+ export const ACCENTS = DEFAULT_ACCENTS;
41
+ /**
42
+ * Binds the theme and accent helpers to one product's accents, default and storage namespace.
43
+ *
44
+ * Three things used to be hardcoded here — the fallback accent, the list of accents, and the
45
+ * `localStorage` prefix — and all three were the same mistake: a shared package holding one
46
+ * product's identity. They move together rather than one at a time, because a product that can
47
+ * choose its default and not its palette has not really chosen anything.
48
+ *
49
+ * **A factory rather than a `configure()` call.** Module-level mutable state would have to be
50
+ * written before the first read, which makes correctness depend on import order — the same
51
+ * class of bug as this library's load-bearing CSS import order, and just as silent. An instance
52
+ * cannot be read before it is made.
53
+ *
54
+ * ```ts
55
+ * export const theming = createTheming({
56
+ * accents: ['teal', 'crimson'],
57
+ * defaultAccent: 'teal',
58
+ * storagePrefix: 'myapp',
59
+ * });
60
+ *
61
+ * theming.readAccent(); // 'teal' | 'crimson'
62
+ * ```
63
+ *
64
+ * `defaultAccent` is typed as a member of `accents`, so naming one that is not on the list is a
65
+ * compile error (`TS2322`) rather than a colourless first paint.
66
+ *
67
+ * The literal array needs no `as const` — the `const` type parameter infers the tuple. A list
68
+ * declared separately *does*, because `const list = ['teal']` widens to `string[]`, which is not
69
+ * a tuple and will be rejected: either inline it or write `as const` there.
70
+ */
71
+ export function createTheming(config) {
72
+ const { accents, defaultAccent, storagePrefix } = config;
73
+ const themeKey = `${storagePrefix}.theme`;
74
+ const accentKey = `${storagePrefix}.accent`;
75
+ return {
76
+ accents,
77
+ defaultAccent,
78
+ readTheme() {
79
+ try {
80
+ const stored = localStorage.getItem(themeKey);
81
+ return THEMES.includes(stored) ? stored : 'system';
82
+ }
83
+ catch {
84
+ // Private windows and blocked site data both throw. A theme preference is not worth a
85
+ // crash, so the system default stands.
86
+ return 'system';
87
+ }
88
+ },
89
+ storeTheme(theme) {
90
+ try {
91
+ localStorage.setItem(themeKey, theme);
92
+ }
93
+ catch {
94
+ // See readTheme. It applies for this session either way.
95
+ }
96
+ },
97
+ readAccent() {
98
+ try {
99
+ const stored = localStorage.getItem(accentKey);
100
+ // Validated against the configured list, so a stored accent that was removed from a
101
+ // later release falls back rather than being handed to Radix as an unknown colour.
102
+ return accents.includes(stored) ? stored : defaultAccent;
103
+ }
104
+ catch {
105
+ // Same as readTheme: a private window throws, and a colour is not worth a crash.
106
+ return defaultAccent;
107
+ }
108
+ },
109
+ storeAccent(accent) {
110
+ try {
111
+ localStorage.setItem(accentKey, accent);
112
+ }
113
+ catch {
114
+ // It applies for this session either way.
115
+ }
116
+ },
117
+ };
72
118
  }
119
+ /**
120
+ * The instance the original free functions are bound to: the six default accents, jade, and the
121
+ * `upfront.*` storage keys the apps that predate `createTheming` already wrote.
122
+ *
123
+ * **One implementation, not two.** These exports exist so no call site had to change when the
124
+ * factory landed, and keeping the prefix is what stops an existing user losing the theme they
125
+ * picked. A new product should call `createTheming` with its own name instead.
126
+ */
127
+ const legacy = createTheming({
128
+ accents: DEFAULT_ACCENTS,
129
+ defaultAccent: 'jade',
130
+ storagePrefix: 'upfront',
131
+ });
132
+ export const readTheme = legacy.readTheme;
133
+ export const storeTheme = legacy.storeTheme;
134
+ export const readAccent = legacy.readAccent;
135
+ export const storeAccent = legacy.storeAccent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kennycorrea/ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Upfront's house component layer: atoms over Radix Themes, the token bridge, and the accent logic.",
5
5
  "private": false,
6
6
  "type": "module",
package/tokens.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Upfront's token bridge — the shared half of every app's stylesheet.
2
+ * The house token bridge — the shared half of every consuming app's stylesheet.
3
3
  *
4
4
  * Two apps said `text-ink-muted` and `bg-brand-700/8` and each carried its own copy of what
5
5
  * those meant. The values below are the dashboard's, which was the more complete of the two.
@@ -36,7 +36,7 @@
36
36
 
37
37
 
38
38
  /**
39
- * Upfront's token names, registered with Tailwind.
39
+ * The house token names, registered with Tailwind.
40
40
  *
41
41
  * The values here are static fallbacks, not the live palette — the real assignments are in
42
42
  * the `.radix-themes` rule below. Two blocks rather than one because of how custom
@@ -88,7 +88,7 @@
88
88
  }
89
89
 
90
90
  /**
91
- * The live bridge: Upfront's names, pointed at Radix Themes' palette.
91
+ * The live bridge: the house names, pointed at Radix Themes' palette.
92
92
  *
93
93
  * Every colour in the app now comes from Themes. This is the only place that names a Radix
94
94
  * variable — the components keep saying `brand-700`, `ink-muted`, `surface`, and nothing