@evinvest/uikit 0.17.0 → 0.18.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.
package/README.md CHANGED
@@ -51,7 +51,7 @@ your Tailwind v4 entrypoint — this is the load-bearing part of the kit:
51
51
  | surfaces | `background` `card` `popover` `muted` `hover` |
52
52
  | ink | `ink` `ink-mid` `ink-soft` — hierarchy, loudest first |
53
53
  | lines | `border` `input` `ring` |
54
- | roles | `brand` `primary` `secondary` `positive` `accent-trace` `accent-debug` `accent-info` `accent-warn` `accent-error`, each with `on-*` where it gets filled |
54
+ | roles | `brand` `primary` `secondary` `positive` `accent-trace` `accent-debug` `accent-info` `accent-warn` `accent-error`, each with `on-*` where it gets filled; `primary-ink` — the primary role as ink on a surface (`#128377` fill under a white `on-primary`, `#2a9d8f` ink) |
55
55
  | scalars | `radius` `control-radius` `control-py` `display-scale` `band-py` `page-max` `page-px` `shadow-*` `font-*` |
56
56
  | charts | `chart-1` … `chart-5` |
57
57
 
@@ -63,6 +63,17 @@ A surface takes its ink from the scope, so it carries no `-foreground`. A
63
63
  **filled role** does not — gold wants black and navy wants white regardless of
64
64
  polarity — so `on-{role}` exists exactly there.
65
65
 
66
+ A fill and an ink are different jobs, and `primary` is the one role that
67
+ names them separately: `primary` is the fill (`bg-primary text-on-primary` on
68
+ a button, a checked switch or checkbox — `#128377`, white reads on it at
69
+ 4.63:1 and it clears 3:1 against `background`, `secondary`, `card` and
70
+ `popover`) and `primary-ink` is the same hue as ink (`text-primary-ink` on a
71
+ link or eyebrow, a radio dot, a slider range, `border-primary-ink` on a checked
72
+ outline, the focus `ring` — `#2a9d8f`, 5.85:1 on `background`, 4.72:1 on
73
+ `card`). Every other role holds its own label and reads as ink with one value.
74
+ A consumer sheet must define `--primary-ink`; see
75
+ [`docs/spec/accents.md`](../../docs/spec/accents.md).
76
+
66
77
  Accents are decorative and **ordered by significance**, quiet to loud. Pick a
67
78
  rung by how loud the thing should be, never by what it means; see
68
79
  [`docs/spec/accents.md`](../../docs/spec/accents.md). Every consumer defines all
@@ -78,11 +89,6 @@ state, never its appearance (`Red`) and never its selection (`Default`); see
78
89
  and carries EV's values: a single dark palette on `:root, .dark`. A consumer
79
90
  with two polarities writes its own sheet against the same names.
80
91
 
81
- `text-main-mist`, `bg-sidebar`, `text-muted-foreground` and the rest keep
82
- working, and the current tokens are re-pointed at the old values so rendered
83
- colour is unchanged. Import it instead while migrating; it goes away once every
84
- consumer has moved.
85
-
86
92
  ## Usage
87
93
 
88
94
  ```tsx
@@ -188,8 +194,10 @@ measuring needs host-only `web-sys`). Known gaps:
188
194
  at day granularity (days outside render `disabled` + `data-disabled="true"`);
189
195
  `disabled` freezes the whole grid and the nav the same way; the nav buttons
190
196
  take label overrides. TS-only `locale` renders the caption and weekday headers through
191
- `Intl` (Monday-first); Rust has no `Intl`, so its captions stay English. Rust
192
- does manual date math; TS uses the built-in `Date`.
197
+ `Intl` (Monday-first): `weekday: "short"`, falling back to `"narrow"` for a
198
+ locale whose short form contains whitespace (vi's "Thứ 2" → "T2", he's
199
+ "יום ב׳" → "ב׳") as long as narrow still yields seven distinct labels. Rust has no `Intl`, so its captions
200
+ stay English. Rust does manual date math; TS uses the built-in `Date`.
193
201
  - **date-time-picker:** the kit's own bricks only — an outline trigger, a
194
202
  `Popover` with the `Calendar` and two numeric 24-hour hours/minutes fields (no
195
203
  native `datetime-local` / `time` input, so the browser's locale popup never
package/dist/index.d.ts CHANGED
@@ -254,7 +254,7 @@ declare const buttonVariantClasses: {
254
254
  readonly outline: "border bg-transparent shadow-xs hover:bg-hover hover:text-ink";
255
255
  readonly ghost: "hover:bg-hover hover:text-ink";
256
256
  readonly destructive: "bg-accent-error text-on-accent-error hover:bg-accent-error/90 focus-visible:ring-accent-error/20";
257
- readonly link: "text-primary underline-offset-4 hover:underline";
257
+ readonly link: "text-primary-ink underline-offset-4 hover:underline";
258
258
  };
259
259
  type ButtonVariant = keyof typeof buttonVariantClasses;
260
260
  declare const buttonSizeClasses: {
package/dist/index.js CHANGED
@@ -234,9 +234,19 @@ function useFloating(opts) {
234
234
 
235
235
  // src/primitives/focus-scope.tsx
236
236
  import * as React6 from "react";
237
- var FOCUSABLE = 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
237
+ var FOCUSABLE = [
238
+ "a[href]",
239
+ "button:not([disabled])",
240
+ "textarea:not([disabled])",
241
+ "input:not([disabled])",
242
+ "select:not([disabled])",
243
+ "[tabindex]"
244
+ ].map((sel) => `${sel}:not([tabindex="-1"])`).join(",");
245
+ function focusCandidates(root) {
246
+ return Array.from(root.querySelectorAll(FOCUSABLE));
247
+ }
238
248
  function focusable(root) {
239
- return Array.from(root.querySelectorAll(FOCUSABLE)).filter(
249
+ return focusCandidates(root).filter(
240
250
  (el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement
241
251
  );
242
252
  }
@@ -708,7 +718,7 @@ var buttonVariantClasses = {
708
718
  "outline": "border bg-transparent shadow-xs hover:bg-hover hover:text-ink",
709
719
  "ghost": "hover:bg-hover hover:text-ink",
710
720
  "destructive": "bg-accent-error text-on-accent-error hover:bg-accent-error/90 focus-visible:ring-accent-error/20",
711
- "link": "text-primary underline-offset-4 hover:underline"
721
+ "link": "text-primary-ink underline-offset-4 hover:underline"
712
722
  };
713
723
  var buttonSizeClasses = {
714
724
  "xs": "h-7 gap-1 px-2 text-xs has-[>svg]:px-1.5",
@@ -930,7 +940,7 @@ var emptyMediaVariants = {
930
940
  "icon": "bg-muted text-ink flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6"
931
941
  };
932
942
  var EMPTY_TITLE = "text-lg font-medium tracking-tight";
933
- var EMPTY_DESCRIPTION = "text-ink-soft [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4";
943
+ var EMPTY_DESCRIPTION = "text-ink-soft [&>a:hover]:text-primary-ink text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4";
934
944
  var EMPTY_CONTENT = "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance";
935
945
 
936
946
  // src/components/empty.tsx
@@ -1032,9 +1042,9 @@ var FIELD_SET = "flex flex-col gap-6 has-[>[data-slot=checkbox-group]]:gap-3 has
1032
1042
  var FIELD_LEGEND = "mb-3 font-medium data-[variant=legend]:text-base data-[variant=label]:text-sm";
1033
1043
  var FIELD_GROUP = "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4";
1034
1044
  var FIELD_CONTENT = "group/field-content flex flex-1 flex-col gap-1.5 leading-snug";
1035
- var FIELD_LABEL = "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4 has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary";
1045
+ var FIELD_LABEL = "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4 has-data-[state=checked]:bg-primary-ink/5 has-data-[state=checked]:border-primary-ink";
1036
1046
  var FIELD_TITLE = "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50";
1037
- var FIELD_DESCRIPTION = "text-ink-soft text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5 [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4";
1047
+ var FIELD_DESCRIPTION = "text-ink-soft text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5 [&>a:hover]:text-primary-ink [&>a]:underline [&>a]:underline-offset-4";
1038
1048
  var FIELD_SEPARATOR = "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2";
1039
1049
  var FIELD_SEPARATOR_LINE = "absolute inset-0 top-1/2 shrink-0 bg-border h-px w-full";
1040
1050
  var FIELD_SEPARATOR_CONTENT = "bg-background text-ink-soft relative mx-auto block w-fit px-2";
@@ -1643,7 +1653,7 @@ var itemMediaVariants = {
1643
1653
  };
1644
1654
  var ITEM_CONTENT = "flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none";
1645
1655
  var ITEM_TITLE = "flex w-fit items-center gap-2 text-sm leading-snug font-medium";
1646
- var ITEM_DESCRIPTION = "text-ink-soft line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4";
1656
+ var ITEM_DESCRIPTION = "text-ink-soft line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary-ink [&>a]:underline [&>a]:underline-offset-4";
1647
1657
  var ITEM_ACTIONS = "flex items-center gap-2";
1648
1658
  var ITEM_HEADER = "flex basis-full items-center justify-between gap-2";
1649
1659
  var ITEM_FOOTER = "flex basis-full items-center justify-between gap-2";
@@ -1947,8 +1957,8 @@ function PaginationEllipsis({
1947
1957
  import "react";
1948
1958
 
1949
1959
  // src/generated/progress.ts
1950
- var PROGRESS_TRACK = "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full";
1951
- var PROGRESS_INDICATOR = "bg-primary h-full w-full flex-1 transition-all";
1960
+ var PROGRESS_TRACK = "bg-primary-ink/20 relative h-2 w-full overflow-hidden rounded-full";
1961
+ var PROGRESS_INDICATOR = "bg-primary-ink h-full w-full flex-1 transition-all";
1952
1962
 
1953
1963
  // src/components/progress.tsx
1954
1964
  import { jsx as jsx22 } from "react/jsx-runtime";
@@ -2466,7 +2476,7 @@ var SECTION_BASE = "w-full px-[var(--page-px)]";
2466
2476
  var SECTION_PY = "py-[var(--band-py)]";
2467
2477
  var SECTION_PY_TIGHT = "py-[var(--band-py-tight)]";
2468
2478
  var SECTION_HEAD = "flex flex-col gap-2 md:gap-3.5";
2469
- var EYEBROW = "font-medium text-[10px] md:text-[11.5px] tracking-[0.15em] md:tracking-[0.16em] text-primary";
2479
+ var EYEBROW = "font-medium text-[10px] md:text-[11.5px] tracking-[0.15em] md:tracking-[0.16em] text-primary-ink [.bg-primary_&]:text-on-primary";
2470
2480
  var DISPLAY_BASE = "font-display [font-weight:700] max-w-[54rem] tracking-[-0.005em] leading-[1.16] md:leading-[1.14] text-[calc(1.5625rem*var(--display-scale))] md:text-[calc(2.625rem*var(--display-scale))]";
2471
2481
  var LEDE = "max-w-[52rem] text-[15px] md:text-[17px] leading-[1.58] text-ink-soft";
2472
2482
  var PROSE = "text-[14px] md:text-[15px] leading-[1.62] text-ink-soft";
@@ -2924,7 +2934,7 @@ import * as React45 from "react";
2924
2934
 
2925
2935
  // src/generated/radio-group.ts
2926
2936
  var RADIO_GROUP_ROOT = "grid gap-3";
2927
- var RADIO_GROUP_ITEM = "border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-accent-error/20 aria-invalid:border-accent-error aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50";
2937
+ var RADIO_GROUP_ITEM = "border-input text-primary-ink focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-accent-error/20 aria-invalid:border-accent-error aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50";
2928
2938
 
2929
2939
  // src/components/radio-group.tsx
2930
2940
  import { jsx as jsx35 } from "react/jsx-runtime";
@@ -3018,7 +3028,7 @@ function RadioGroupItem({
3018
3028
  children: /* @__PURE__ */ jsx35(
3019
3029
  "svg",
3020
3030
  {
3021
- className: "fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2",
3031
+ className: "fill-primary-ink absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2",
3022
3032
  viewBox: "0 0 24 24",
3023
3033
  "aria-hidden": "true",
3024
3034
  children: /* @__PURE__ */ jsx35("circle", { cx: "12", cy: "12", r: "10" })
@@ -3036,8 +3046,8 @@ import * as React46 from "react";
3036
3046
  // src/generated/slider.ts
3037
3047
  var SLIDER_ROOT = "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col";
3038
3048
  var SLIDER_TRACK = "bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5";
3039
- var SLIDER_RANGE = "bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full";
3040
- var SLIDER_THUMB = "border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-ink shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50";
3049
+ var SLIDER_RANGE = "bg-primary-ink absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full";
3050
+ var SLIDER_THUMB = "border-primary-ink ring-ring/50 block size-4 shrink-0 rounded-full border bg-ink shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50";
3041
3051
 
3042
3052
  // src/components/slider.tsx
3043
3053
  import { jsx as jsx36, jsxs as jsxs9 } from "react/jsx-runtime";
@@ -6970,7 +6980,7 @@ var CALENDAR_NAV = "flex items-center justify-between gap-1 w-full px-1";
6970
6980
  var CALENDAR_CAPTION = "text-sm font-medium select-none";
6971
6981
  var CALENDAR_GRID = "w-full border-collapse mt-4";
6972
6982
  var CALENDAR_WEEKDAY_ROW = "flex";
6973
- var CALENDAR_WEEKDAY = "text-ink-soft rounded-md w-9 shrink-0 font-normal text-[0.8rem] select-none";
6983
+ var CALENDAR_WEEKDAY = "text-ink-soft rounded-md w-9 shrink-0 font-normal text-[0.8rem] select-none whitespace-nowrap";
6974
6984
  var CALENDAR_WEEK = "flex w-full mt-2";
6975
6985
  var CALENDAR_DAY_EMPTY = "relative size-9 shrink-0 p-0 select-none";
6976
6986
  var CALENDAR_DAY_CELL = "relative size-9 shrink-0 p-0 text-center select-none";
@@ -7033,13 +7043,19 @@ function outOfRange(day, min, max) {
7033
7043
  return !!min && t < startOfDay(min).getTime() || !!max && t > startOfDay(max).getTime();
7034
7044
  }
7035
7045
  var MONDAY = new Date(2024, 0, 1);
7036
- function localisedWeekdays(locale) {
7037
- const fmt = new Intl.DateTimeFormat(locale, { weekday: "short" });
7046
+ function formatWeekdays(locale, weekday) {
7047
+ const fmt = new Intl.DateTimeFormat(locale, { weekday });
7038
7048
  return Array.from(
7039
7049
  { length: 7 },
7040
7050
  (_, i) => fmt.format(new Date(MONDAY.getFullYear(), MONDAY.getMonth(), MONDAY.getDate() + i))
7041
7051
  );
7042
7052
  }
7053
+ function localisedWeekdays(locale) {
7054
+ const short = formatWeekdays(locale, "short");
7055
+ if (!short.some((label) => /\s/.test(label))) return short;
7056
+ const narrow = formatWeekdays(locale, "narrow");
7057
+ return new Set(narrow).size === narrow.length ? narrow : short;
7058
+ }
7043
7059
  function Calendar({
7044
7060
  selected,
7045
7061
  onSelect,
@@ -7289,6 +7305,7 @@ function DateTimePicker({
7289
7305
  });
7290
7306
  const rootRef = React67.useRef(null);
7291
7307
  const hoursRef = React67.useRef(null);
7308
+ const focusTrigger = () => rootRef.current?.querySelector("[data-slot=date-time-picker-trigger]")?.focus();
7292
7309
  React67.useEffect(() => {
7293
7310
  if (!isOpen) return;
7294
7311
  hoursRef.current?.focus();
@@ -7296,9 +7313,7 @@ function DateTimePicker({
7296
7313
  const active = document.activeElement;
7297
7314
  const content = hoursRef.current?.closest("[data-slot=date-time-picker-content]");
7298
7315
  const lost = !active || active === document.body || !!content?.contains(active);
7299
- if (lost) {
7300
- rootRef.current?.querySelector("[data-slot=date-time-picker-trigger]")?.focus();
7301
- }
7316
+ if (lost) focusTrigger();
7302
7317
  };
7303
7318
  }, [isOpen]);
7304
7319
  const emit = (next) => setValue(clampMoment(atMinute(next), min, max));
@@ -7330,6 +7345,15 @@ function DateTimePicker({
7330
7345
  setValue(null);
7331
7346
  setOpen(false);
7332
7347
  };
7348
+ const onContentKeyDown = (e) => {
7349
+ if (e.key !== "Tab") return;
7350
+ const items = focusCandidates(e.currentTarget);
7351
+ const edge = e.shiftKey ? items[0] : items[items.length - 1];
7352
+ if (!edge || e.target !== edge) return;
7353
+ e.preventDefault();
7354
+ setOpen(false);
7355
+ focusTrigger();
7356
+ };
7333
7357
  const onContentBlur = (e) => {
7334
7358
  const next = e.relatedTarget;
7335
7359
  if (!(next instanceof Node)) return;
@@ -7383,6 +7407,7 @@ function DateTimePicker({
7383
7407
  "aria-label": labels.dialog ?? "Choose date and time",
7384
7408
  align,
7385
7409
  onBlur: onContentBlur,
7410
+ onKeyDown: onContentKeyDown,
7386
7411
  className: DATE_TIME_PICKER_CONTENT,
7387
7412
  children: [
7388
7413
  /* @__PURE__ */ jsx57(