@nextlyhq/ui 0.0.2-alpha.58 → 0.0.2-alpha.60

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/dist/index.cjs CHANGED
@@ -114,7 +114,11 @@ __export(src_exports, {
114
114
  DropdownMenuSubContent: () => DropdownMenuSubContent,
115
115
  DropdownMenuSubTrigger: () => DropdownMenuSubTrigger,
116
116
  DropdownMenuTrigger: () => DropdownMenuTrigger,
117
+ FieldShell: () => FieldShell,
118
+ FormActions: () => FormActions,
117
119
  FormLabelWithTooltip: () => FormLabelWithTooltip,
120
+ FormLayout: () => FormLayout,
121
+ FormSection: () => FormSection,
118
122
  Grid: () => Grid,
119
123
  Input: () => Input,
120
124
  Label: () => Label,
@@ -489,7 +493,15 @@ var badgeVariants = (0, import_class_variance_authority3.cva)(
489
493
  success: "bg-success-100 text-success-700 dark:bg-success-900 dark:text-success-100",
490
494
  warning: "bg-warning-100 text-warning-700 dark:bg-warning-900 dark:text-warning-100",
491
495
  destructive: "bg-destructive-100 text-destructive-700 dark:bg-destructive-900 dark:text-destructive-100",
492
- outline: "border border-border! bg-transparent text-foreground dark:text-muted-foreground dark:border-border!"
496
+ /*
497
+ * The border colour is NOT important-marked. The base sets no border
498
+ * colour, so nothing inside this component competes for it — the only
499
+ * thing a mark could win against is the caller's own class. Seven
500
+ * outline badges pass one, including a status pill whose comment gives
501
+ * a 3:1 contrast reason for the colour it asks for, and every one of
502
+ * them was being overridden without any sign that it had been.
503
+ */
504
+ outline: "border border-border bg-transparent text-foreground dark:text-muted-foreground dark:border-border"
493
505
  }
494
506
  },
495
507
  defaultVariants: {
@@ -665,6 +677,13 @@ var COLS = {
665
677
  4: "grid-cols-4",
666
678
  6: "grid-cols-6"
667
679
  };
680
+ var RESPONSIVE_COLS = {
681
+ 1: "grid-cols-1",
682
+ 2: "grid-cols-1 @2xl:grid-cols-2",
683
+ 3: "grid-cols-1 @2xl:grid-cols-3",
684
+ 4: "grid-cols-1 @2xl:grid-cols-4",
685
+ 6: "grid-cols-1 @2xl:grid-cols-6"
686
+ };
668
687
  var Stack = (0, import_react7.forwardRef)(
669
688
  ({ direction = "col", gap = 4, className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
670
689
  "div",
@@ -682,14 +701,20 @@ var Stack = (0, import_react7.forwardRef)(
682
701
  );
683
702
  Stack.displayName = "Stack";
684
703
  var Grid = (0, import_react7.forwardRef)(
685
- ({ cols = 2, gap = 4, className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
686
- "div",
687
- {
688
- ref,
689
- className: cn("grid", COLS[cols], GAP[gap], className),
690
- ...props
704
+ ({ cols = 2, gap = 4, responsive = false, className, children, ...rest }, ref) => {
705
+ if (!responsive) {
706
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
707
+ "div",
708
+ {
709
+ ref,
710
+ className: cn("grid", COLS[cols], GAP[gap], className),
711
+ ...rest,
712
+ children
713
+ }
714
+ );
691
715
  }
692
- )
716
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { ref, className: cn("@container", className), ...rest, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: cn("grid", RESPONSIVE_COLS[cols], GAP[gap]), children }) });
717
+ }
693
718
  );
694
719
  Grid.displayName = "Grid";
695
720
  var Stat = (0, import_react7.forwardRef)(
@@ -1149,7 +1174,20 @@ var tabsListVariants = (0, import_class_variance_authority8.cva)(
1149
1174
  // `gap-1` and `p-0` are invariant across the variants, so they belong here —
1150
1175
  // repeating them in each arm gives any future adjustment two sites to change
1151
1176
  // and one to forget.
1152
- "inline-flex items-center justify-center gap-1 rounded-none p-0 text-muted-foreground",
1177
+ //
1178
+ // The rail the triggers sit on, and it is load-bearing rather than
1179
+ // decoration. Each trigger draws a 2px border on its trailing edge and pulls
1180
+ // itself half a step onto this one: with no rail here, that pull put a 2px
1181
+ // line onto whatever followed the strip in the document, and above a rounded
1182
+ // panel that is the corner curve — a straight bar crossing an 8px arc. The
1183
+ // rail also gives an inactive tab somewhere to be inactive, which is what
1184
+ // makes the active one read as selected rather than as the only tab.
1185
+ //
1186
+ // Which edge is the trailing one depends on orientation, so the rail follows
1187
+ // it. Radix stamps `data-orientation` on the list and on every trigger, so
1188
+ // both halves read the same source and cannot disagree about the axis. A
1189
+ // fixed bottom rail draws a horizontal line beneath a vertical list.
1190
+ "inline-flex items-center justify-center gap-1 rounded-none border-b border-border p-0 text-muted-foreground data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch data-[orientation=vertical]:border-b-0 data-[orientation=vertical]:border-r",
1153
1191
  {
1154
1192
  variants: { variant: TABS_LIST_VARIANTS },
1155
1193
  defaultVariants: { variant: "default" }
@@ -1170,9 +1208,22 @@ var TABS_TRIGGER_SIZES = {
1170
1208
  sm: "text-xs"
1171
1209
  };
1172
1210
  var tabsTriggerVariants = (0, import_class_variance_authority8.cva)(
1173
- // Square corners: the active state is a 2px bottom border that has to run the
1174
- // full width of the trigger.
1175
- "inline-flex items-center justify-center whitespace-nowrap rounded-none bg-transparent px-4 py-2 font-medium cursor-pointer transition-all duration-200 border-b-2 relative -mb-0.5 data-[state=active]:border-b-primary! data-[state=active]:text-primary data-[state=inactive]:border-transparent data-[state=inactive]:text-muted-foreground hover:text-primary hover:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed",
1211
+ // Square corners: the active state is a 2px border on the trailing edge that
1212
+ // has to run the full length of the trigger and stay flush with its ends.
1213
+ //
1214
+ // The active colour is NOT important-marked. Nothing inside this component
1215
+ // competes for it — the inactive arm is mutually exclusive with the active
1216
+ // one — so the only thing an `!` could win against is a caller's class, which
1217
+ // is precisely what a theme supplies. Marking it made the one line under
1218
+ // every selected tab the one line a theme could not restyle, and the failure
1219
+ // read as "the theme did not apply here" rather than as an opt-out.
1220
+ //
1221
+ // The trailing edge is the bottom when the strip is horizontal and the right
1222
+ // when it is vertical, so the indicator, the half-step pull onto the rail and
1223
+ // the active colour all switch axis together off the same `data-orientation`
1224
+ // the list reads. Switching only some of them puts the selection affordance
1225
+ // on one axis and the line it sits on the other.
1226
+ "inline-flex items-center justify-center whitespace-nowrap rounded-none bg-transparent px-4 py-2 font-medium cursor-pointer transition-all duration-200 border-b-2 relative -mb-0.5 data-[state=active]:border-b-primary data-[state=active]:text-primary data-[state=inactive]:border-transparent data-[state=inactive]:text-muted-foreground hover:text-primary hover:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed data-[orientation=vertical]:border-b-0 data-[orientation=vertical]:mb-0 data-[orientation=vertical]:border-r-2 data-[orientation=vertical]:-mr-0.5 data-[orientation=vertical]:data-[state=active]:border-r-primary",
1176
1227
  {
1177
1228
  variants: { size: TABS_TRIGGER_SIZES },
1178
1229
  defaultVariants: { size: "default" }
@@ -1790,7 +1841,9 @@ var DialogPrimitive2 = __toESM(require("@radix-ui/react-dialog"), 1);
1790
1841
  var import_class_variance_authority11 = require("class-variance-authority");
1791
1842
  var React7 = __toESM(require("react"), 1);
1792
1843
  var import_jsx_runtime26 = require("react/jsx-runtime");
1793
- var Sheet = DialogPrimitive2.Root;
1844
+ var SheetModalContext = React7.createContext(true);
1845
+ var Sheet = ({ modal = true, children, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SheetModalContext.Provider, { value: modal, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(DialogPrimitive2.Root, { modal, ...props, children }) });
1846
+ Sheet.displayName = "Sheet";
1794
1847
  var SheetTrigger = DialogPrimitive2.Trigger;
1795
1848
  var SheetClose = DialogPrimitive2.Close;
1796
1849
  var SheetPortal = DialogPrimitive2.Portal;
@@ -1831,8 +1884,9 @@ var sheetVariants = (0, import_class_variance_authority11.cva)(
1831
1884
  );
1832
1885
  var SheetContent = React7.forwardRef(({ side = "right", className, children, ...props }, ref) => {
1833
1886
  const portalContainer = usePortalContainer();
1887
+ const modal = React7.useContext(SheetModalContext);
1834
1888
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(SheetPortal, { container: portalContainer, children: [
1835
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SheetOverlay, {}),
1889
+ modal ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SheetOverlay, {}) : null,
1836
1890
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1837
1891
  DialogPrimitive2.Content,
1838
1892
  {
@@ -4066,6 +4120,172 @@ function useActiveShortcuts() {
4066
4120
  manager.activeBindings
4067
4121
  );
4068
4122
  }
4123
+
4124
+ // src/components/field-shell.tsx
4125
+ var import_class_variance_authority13 = require("class-variance-authority");
4126
+ var import_react19 = require("react");
4127
+ var import_jsx_runtime40 = require("react/jsx-runtime");
4128
+ var controlWidth = (0, import_class_variance_authority13.cva)("w-full", {
4129
+ variants: {
4130
+ width: {
4131
+ half: "max-w-[var(--nx-field-half,380px)]",
4132
+ full: "max-w-[var(--nx-field-full,760px)]",
4133
+ fill: ""
4134
+ }
4135
+ },
4136
+ defaultVariants: { width: "half" }
4137
+ });
4138
+ function readControlProps(child) {
4139
+ const props = child.props;
4140
+ return props && typeof props === "object" ? props : {};
4141
+ }
4142
+ function isRenderFunction(children) {
4143
+ return typeof children === "function";
4144
+ }
4145
+ function computeControlWiring(ownProps, requestedId, renderedMessageIds, hasError) {
4146
+ const ownId = typeof ownProps.id === "string" && ownProps.id !== "" ? ownProps.id : void 0;
4147
+ const id = ownId ?? requestedId;
4148
+ const ownDescribedByIds = typeof ownProps["aria-describedby"] === "string" ? ownProps["aria-describedby"].split(" ").filter(Boolean) : [];
4149
+ const describedByIds = Array.from(
4150
+ /* @__PURE__ */ new Set([...ownDescribedByIds, ...renderedMessageIds])
4151
+ );
4152
+ const describedBy = describedByIds.length > 0 ? describedByIds.join(" ") : void 0;
4153
+ const ownInvalid = typeof ownProps["aria-invalid"] === "boolean" ? ownProps["aria-invalid"] : void 0;
4154
+ const invalid = hasError ? true : ownInvalid;
4155
+ return { id, describedBy, invalid };
4156
+ }
4157
+ function FieldShell({
4158
+ label,
4159
+ description,
4160
+ error,
4161
+ width = "half",
4162
+ htmlFor,
4163
+ className,
4164
+ children
4165
+ }) {
4166
+ const generatedControlId = (0, import_react19.useId)();
4167
+ const messageBaseId = (0, import_react19.useId)();
4168
+ const descriptionId = `${messageBaseId}-description`;
4169
+ const errorId = `${messageBaseId}-error`;
4170
+ const requestedId = htmlFor ?? generatedControlId;
4171
+ const renderedMessageIds = [
4172
+ description ? descriptionId : null,
4173
+ error ? errorId : null
4174
+ ].filter((id) => id !== null);
4175
+ const wiring = isRenderFunction(children) ? computeControlWiring({}, requestedId, renderedMessageIds, Boolean(error)) : computeControlWiring(
4176
+ readControlProps(children),
4177
+ requestedId,
4178
+ renderedMessageIds,
4179
+ Boolean(error)
4180
+ );
4181
+ const isFragmentChild = !isRenderFunction(children) && children.type === import_react19.Fragment;
4182
+ (0, import_react19.useEffect)(() => {
4183
+ if (!isDevelopmentRuntime()) return;
4184
+ if (isFragmentChild) return;
4185
+ const landed = document.getElementById(wiring.id) !== null;
4186
+ const fieldName = typeof label === "string" ? `"${label}"` : "this field";
4187
+ devWarnOnce(
4188
+ landed,
4189
+ `FieldShell: the id computed for ${fieldName} never appeared on any element in the document, so its label and any description are not connected to the control. This happens when the control is a compound component \u2014 a Radix \`Select\`, \`RadioGroup\`, or similar \u2014 whose root does not forward \`id\`/\`aria-describedby\`/\`aria-invalid\` to a real DOM node. Pass \`children\` as a function and apply those to the control's actual focusable element (its trigger) instead of a single top-level clone.`
4190
+ );
4191
+ }, [wiring.id, label, isFragmentChild]);
4192
+ let control;
4193
+ if (isRenderFunction(children)) {
4194
+ control = children(wiring);
4195
+ } else {
4196
+ devWarnOnce(
4197
+ !isFragmentChild,
4198
+ "FieldShell: `children` must be a single element, not a Fragment. A Fragment forwards no props to the elements inside it, so the id, aria-describedby and aria-invalid this component computes would have nowhere to land \u2014 the label and the control would look wired up and never actually connect. Wrap the intended elements in a real element instead, such as a `<div>`."
4199
+ );
4200
+ const controlOverrides = {
4201
+ id: wiring.id,
4202
+ ...wiring.describedBy !== void 0 ? { "aria-describedby": wiring.describedBy } : {},
4203
+ ...wiring.invalid !== void 0 ? { "aria-invalid": wiring.invalid } : {}
4204
+ };
4205
+ control = isFragmentChild ? children : (0, import_react19.cloneElement)(children, controlOverrides);
4206
+ }
4207
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("div", { className: cn("flex flex-col gap-1.5", className), children: [
4208
+ label ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
4209
+ "label",
4210
+ {
4211
+ htmlFor: wiring.id,
4212
+ className: "text-sm font-medium text-foreground",
4213
+ children: label
4214
+ }
4215
+ ) : null,
4216
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: controlWidth({ width }), children: control }),
4217
+ description ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("p", { id: descriptionId, className: "text-sm text-muted-foreground", children: description }) : null,
4218
+ error ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("p", { id: errorId, className: "text-sm font-medium text-destructive", children: error }) : null
4219
+ ] });
4220
+ }
4221
+
4222
+ // src/components/form-section.tsx
4223
+ var import_react20 = require("react");
4224
+ var import_jsx_runtime41 = require("react/jsx-runtime");
4225
+ function FormSection({
4226
+ label,
4227
+ description,
4228
+ className,
4229
+ children
4230
+ }) {
4231
+ const labelId = (0, import_react20.useId)();
4232
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("section", { "aria-labelledby": labelId, className: cn("space-y-2", className), children: [
4233
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "space-y-1", children: [
4234
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
4235
+ "p",
4236
+ {
4237
+ id: labelId,
4238
+ className: "text-xs font-semibold tracking-[0.08em] uppercase text-muted-foreground",
4239
+ children: label
4240
+ }
4241
+ ),
4242
+ description ? /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("p", { className: "text-sm text-muted-foreground", children: description }) : null
4243
+ ] }),
4244
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(Card, { className: "overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "divide-y divide-foreground/10 px-6", children }) })
4245
+ ] });
4246
+ }
4247
+
4248
+ // src/components/form-layout.tsx
4249
+ var import_class_variance_authority14 = require("class-variance-authority");
4250
+ var import_jsx_runtime42 = require("react/jsx-runtime");
4251
+ var measure = (0, import_class_variance_authority14.cva)("mx-auto w-full px-6 py-8", {
4252
+ variants: {
4253
+ width: {
4254
+ form: "max-w-[56rem]",
4255
+ wide: "max-w-[72rem]"
4256
+ }
4257
+ },
4258
+ defaultVariants: { width: "form" }
4259
+ });
4260
+ function FormLayout({
4261
+ width = "form",
4262
+ className,
4263
+ children
4264
+ }) {
4265
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: cn(measure({ width }), className), children });
4266
+ }
4267
+ function FormActions({ dirty, className, children }) {
4268
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
4269
+ "div",
4270
+ {
4271
+ className: cn(
4272
+ "sticky bottom-0 mt-8 flex items-center justify-end gap-3",
4273
+ "border-t border-border bg-background/95 py-4 backdrop-blur",
4274
+ className
4275
+ ),
4276
+ children: [
4277
+ dirty ? (
4278
+ // `role="status"` (implying `aria-live="polite"`) so a screen-reader
4279
+ // user is told when the flag flips false to true, not only sighted
4280
+ // users watching the bar. This is the only live region FormActions
4281
+ // renders, one per form.
4282
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { role: "status", className: "mr-auto text-sm text-muted-foreground", children: "You have unsaved changes" })
4283
+ ) : null,
4284
+ children
4285
+ ]
4286
+ }
4287
+ );
4288
+ }
4069
4289
  // Annotate the CommonJS export names for ESM import in node:
4070
4290
  0 && (module.exports = {
4071
4291
  Accordion,
@@ -4151,7 +4371,11 @@ function useActiveShortcuts() {
4151
4371
  DropdownMenuSubContent,
4152
4372
  DropdownMenuSubTrigger,
4153
4373
  DropdownMenuTrigger,
4374
+ FieldShell,
4375
+ FormActions,
4154
4376
  FormLabelWithTooltip,
4377
+ FormLayout,
4378
+ FormSection,
4155
4379
  Grid,
4156
4380
  Input,
4157
4381
  Label,