@xenosystem/components 0.2.1 → 0.3.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.
@@ -91,10 +91,23 @@ interface CredentialFormProps {
91
91
  before?: ReactNode;
92
92
  /** Rendered under the submit — "Forgot password?", terms, the other-mode link. */
93
93
  after?: ReactNode;
94
+ /** Live password rules (sign-up): shown while the field is focused or partly filled — never before,
95
+ * a list of things you have failed before you typed is a telling-off for a crime not yet committed. */
96
+ passwordRules?: {
97
+ label: string;
98
+ met: boolean;
99
+ }[];
100
+ /** Rendered beside the password label — the page's "Forgot?" link on sign-in. */
101
+ passwordAside?: ReactNode;
102
+ /** When false, the fields collapse (0fr grid) and leave the accessibility tree; the page's disclosure
103
+ * controls it. Default open. */
104
+ open?: boolean;
105
+ /** id for the collapsible region (matched by `EmailDisclosure.controls`). */
106
+ id?: string;
94
107
  /** Copy overrides; defaults are the platform page's. */
95
108
  labels?: Partial<Record<'name' | 'email' | 'password' | 'submitSignIn' | 'submitSignUp' | 'signin' | 'signup', string>>;
96
109
  }
97
- declare function CredentialForm({ mode, onModeChange, values, onChange, errors, submitting, onSubmit, title, body, before, after, labels }: CredentialFormProps): react.JSX.Element;
110
+ declare function CredentialForm({ mode, onModeChange, values, onChange, errors, submitting, onSubmit, title, body, before, after, labels, passwordRules, passwordAside, open, id }: CredentialFormProps): react.JSX.Element;
98
111
 
99
112
  type SocialProvider = 'google' | 'github' | 'x';
100
113
  interface SocialSignInProps {
@@ -109,6 +122,22 @@ declare function OrDivider({ children }: {
109
122
  children?: string;
110
123
  }): react.JSX.Element;
111
124
 
125
+ /**
126
+ * `EmailDisclosure` — the divider that is a BUTTON. On the origin page the email/password form starts
127
+ * collapsed because it is the minority path (most people press a provider), so the "or continue with
128
+ * email" rule opens it. A real button with `aria-expanded`/`aria-controls`, never a styled span, so it is
129
+ * keyboard-reachable and announced as what it is.
130
+ */
131
+ interface EmailDisclosureProps {
132
+ open: boolean;
133
+ onToggle: () => void;
134
+ /** id of the form region it controls. */
135
+ controls: string;
136
+ openLabel?: string;
137
+ closedLabel?: string;
138
+ }
139
+ declare function EmailDisclosure({ open, onToggle, controls, openLabel, closedLabel }: EmailDisclosureProps): react.JSX.Element;
140
+
112
141
  declare const STATES: {
113
142
  id: string;
114
143
  props: SignInFormProps;
@@ -120,4 +149,4 @@ declare const CREDENTIAL_STATES: {
120
149
  props: CredentialStateProps;
121
150
  }[];
122
151
 
123
- export { AuthDoor, type AuthDoorProps, AuthHero, type AuthHeroProps, type AuthHeroStat, CREDENTIAL_STATES, type CredentialErrors, CredentialForm, type CredentialFormProps, type CredentialMode, type CredentialValues, LicencePlate, OrDivider, STATES, type SignInAction, SignInForm, type SignInFormProps, type SocialProvider, SocialSignIn, type SocialSignInProps, XenoMark, ensureAuthStyles };
152
+ export { AuthDoor, type AuthDoorProps, AuthHero, type AuthHeroProps, type AuthHeroStat, CREDENTIAL_STATES, type CredentialErrors, CredentialForm, type CredentialFormProps, type CredentialMode, type CredentialValues, EmailDisclosure, type EmailDisclosureProps, LicencePlate, OrDivider, STATES, type SignInAction, SignInForm, type SignInFormProps, type SocialProvider, SocialSignIn, type SocialSignInProps, XenoMark, ensureAuthStyles };
@@ -43,7 +43,23 @@ var CSS = `
43
43
  .xc-cred{width:100%;max-width:400px;display:flex;flex-direction:column;gap:24px}
44
44
  .xc-cred__head .xc-form__h2{font-size:30px}
45
45
  .xc-cred__mode{width:100%}
46
+ .xc-cred__collapse{display:grid;grid-template-rows:1fr;opacity:1;transition:grid-template-rows var(--xeno-dur-entrance) var(--xeno-ease),opacity var(--xeno-dur-entrance) var(--xeno-ease)}
47
+ .xc-cred__collapse[data-open=false]{grid-template-rows:0fr;opacity:0}
48
+ .xc-cred__collapse > *{overflow:hidden;min-height:0}
46
49
  .xc-cred__fields{display:flex;flex-direction:column;gap:16px}
50
+ .xc-cred__labelRow{display:flex;justify-content:space-between;align-items:baseline}
51
+ .xc-cred__labelRow a{font-size:12px;color:var(--xeno-muted);text-decoration:none}
52
+ .xc-cred__labelRow a:hover{color:var(--xeno-text)}
53
+ .xc-cred__rules{list-style:none;margin:0;padding:0;display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows var(--xeno-dur-entrance) var(--xeno-ease),opacity var(--xeno-dur-entrance) var(--xeno-ease)}
54
+ .xc-cred__rules[data-open=true]{grid-template-rows:1fr;opacity:1;margin-top:10px}
55
+ .xc-cred__rules > li{overflow:hidden;min-height:0}
56
+ .xc-cred__rule{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--xeno-muted);transition:color var(--xeno-dur-fast) var(--xeno-ease)}
57
+ .xc-cred__rule[data-met=true]{color:var(--xeno-text)}
58
+ .xc-cred__ruleMark{width:14px;height:14px;flex:none}
59
+ .xc-or__btn{border:0;cursor:pointer;display:inline-flex;align-items:center;gap:6px;font:inherit;font-size:11px;letter-spacing:.12em;text-transform:uppercase}
60
+ .xc-or__btn:hover{color:var(--xeno-text)}
61
+ .xc-or__caret{width:12px;height:12px;transition:transform var(--xeno-dur-fast) var(--xeno-ease)}
62
+ .xc-or__caret[data-open=true]{transform:rotate(180deg)}
47
63
  .xc-cred__reveal{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows var(--xeno-dur-entrance) var(--xeno-ease),opacity var(--xeno-dur-entrance) var(--xeno-ease)}
48
64
  .xc-cred__reveal[data-open=true]{grid-template-rows:1fr;opacity:1}
49
65
  .xc-cred__reveal > *{overflow:hidden;min-height:0}
@@ -174,8 +190,10 @@ var L = {
174
190
  signin: "Sign In",
175
191
  signup: "Sign Up"
176
192
  };
177
- function CredentialForm({ mode, onModeChange, values, onChange, errors = {}, submitting = false, onSubmit, title, body, before, after, labels }) {
193
+ function CredentialForm({ mode, onModeChange, values, onChange, errors = {}, submitting = false, onSubmit, title, body, before, after, labels, passwordRules, passwordAside, open = true, id = "xc-credential-fields" }) {
178
194
  const [showPassword, setShowPassword] = useState3(false);
195
+ const [passwordFocused, setPasswordFocused] = useState3(false);
196
+ const showRules = mode === "signup" && !!passwordRules?.length && (passwordFocused || values.password.length > 0);
179
197
  const t = { ...L, ...labels };
180
198
  const set = (k) => (e) => onChange({ ...values, [k]: e.target.value });
181
199
  const submit = (e) => {
@@ -199,26 +217,33 @@ function CredentialForm({ mode, onModeChange, values, onChange, errors = {}, sub
199
217
  }
200
218
  ),
201
219
  before,
202
- /* @__PURE__ */ jsxs5("div", { className: "xc-cred__fields", children: [
220
+ /* @__PURE__ */ jsx5("div", { className: "xc-cred__collapse", "data-open": open, id, "aria-hidden": !open, children: /* @__PURE__ */ jsxs5("div", { className: "xc-cred__fields", children: [
203
221
  /* @__PURE__ */ jsx5("div", { className: "xc-cred__reveal", "data-open": mode === "signup", "aria-hidden": mode !== "signup", children: /* @__PURE__ */ jsxs5("label", { className: "xc-cred__field", children: [
204
222
  /* @__PURE__ */ jsx5("span", { className: "xc-cred__label", children: t.name }),
205
- /* @__PURE__ */ jsx5(TextInput, { size: "lg", value: values.name, onChange: set("name"), placeholder: "Your name", autoComplete: "name", tabIndex: mode === "signup" ? 0 : -1, "aria-invalid": !!errors.name, "data-field": "name" }),
223
+ /* @__PURE__ */ jsx5(TextInput, { size: "lg", value: values.name, onChange: set("name"), placeholder: "Your name", autoComplete: "name", tabIndex: mode === "signup" && open ? 0 : -1, "aria-invalid": !!errors.name, "data-field": "name" }),
206
224
  errors.name && /* @__PURE__ */ jsx5("span", { className: "xc-cred__error", role: "alert", children: errors.name })
207
225
  ] }) }),
208
226
  /* @__PURE__ */ jsxs5("label", { className: "xc-cred__field", children: [
209
227
  /* @__PURE__ */ jsx5("span", { className: "xc-cred__label", children: t.email }),
210
- /* @__PURE__ */ jsx5(TextInput, { size: "lg", type: "email", value: values.email, onChange: set("email"), placeholder: "you@example.com", autoComplete: "email", required: true, "aria-invalid": !!errors.email, "data-field": "email" }),
228
+ /* @__PURE__ */ jsx5(TextInput, { size: "lg", type: "email", value: values.email, onChange: set("email"), placeholder: "you@example.com", autoComplete: "email", required: true, tabIndex: open ? 0 : -1, "aria-invalid": !!errors.email, "data-field": "email" }),
211
229
  errors.email && /* @__PURE__ */ jsx5("span", { className: "xc-cred__error", role: "alert", children: errors.email })
212
230
  ] }),
213
231
  /* @__PURE__ */ jsxs5("label", { className: "xc-cred__field", children: [
214
- /* @__PURE__ */ jsx5("span", { className: "xc-cred__label", children: t.password }),
232
+ /* @__PURE__ */ jsxs5("span", { className: "xc-cred__labelRow", children: [
233
+ /* @__PURE__ */ jsx5("span", { className: "xc-cred__label", children: t.password }),
234
+ passwordAside
235
+ ] }),
215
236
  /* @__PURE__ */ jsxs5("span", { className: "xc-cred__pw", children: [
216
- /* @__PURE__ */ jsx5(TextInput, { size: "lg", type: showPassword ? "text" : "password", value: values.password, onChange: set("password"), placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", autoComplete: mode === "signup" ? "new-password" : "current-password", required: true, "aria-invalid": !!errors.password, "data-field": "password" }),
237
+ /* @__PURE__ */ jsx5(TextInput, { size: "lg", type: showPassword ? "text" : "password", value: values.password, onChange: set("password"), onFocus: () => setPasswordFocused(true), onBlur: () => setPasswordFocused(false), placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", autoComplete: mode === "signup" ? "new-password" : "current-password", required: true, "aria-invalid": !!errors.password, "data-field": "password" }),
217
238
  /* @__PURE__ */ jsx5(Button2, { type: "button", variant: "ghost", size: "sm", className: "xc-cred__eye", onClick: () => setShowPassword((v) => !v), "aria-label": showPassword ? "Hide password" : "Show password", "aria-pressed": showPassword, children: showPassword ? "Hide" : "Show" })
218
239
  ] }),
219
- errors.password && /* @__PURE__ */ jsx5("span", { className: "xc-cred__error", role: "alert", children: errors.password })
240
+ errors.password && /* @__PURE__ */ jsx5("span", { className: "xc-cred__error", role: "alert", children: errors.password }),
241
+ passwordRules && /* @__PURE__ */ jsx5("ul", { className: "xc-cred__rules", "data-open": showRules, "aria-live": "polite", children: passwordRules.map((r, i) => /* @__PURE__ */ jsxs5("li", { className: "xc-cred__rule", "data-met": r.met, style: { transitionDelay: `${i * 60}ms` }, children: [
242
+ /* @__PURE__ */ jsx5("svg", { className: "xc-cred__ruleMark", viewBox: "0 0 16 16", "aria-hidden": "true", children: r.met ? /* @__PURE__ */ jsx5("path", { d: "M3.5 8.5l3 3 6-6", fill: "none", stroke: "currentColor", strokeWidth: "1.6" }) : /* @__PURE__ */ jsx5("rect", { x: "4", y: "7.25", width: "8", height: "1.5", fill: "currentColor" }) }),
243
+ /* @__PURE__ */ jsx5("span", { children: r.label })
244
+ ] }, r.label)) })
220
245
  ] })
221
- ] }),
246
+ ] }) }),
222
247
  errors.form && /* @__PURE__ */ jsx5("p", { className: "xc-form__warn", role: "alert", children: errors.form }),
223
248
  /* @__PURE__ */ jsx5("div", { className: "xc-form__actions", children: /* @__PURE__ */ jsx5(Button2, { type: "submit", variant: "primary", size: "lg", busy: submitting, "data-intent": "submit", children: mode === "signin" ? t.submitSignIn : t.submitSignUp }) }),
224
249
  after && /* @__PURE__ */ jsx5("div", { className: "xc-cred__after", children: after })
@@ -249,6 +274,15 @@ function OrDivider({ children = "or continue with email" }) {
249
274
  return /* @__PURE__ */ jsx7("div", { className: "xc-or", role: "separator", "aria-label": children, children: /* @__PURE__ */ jsx7("span", { className: "xc-or__label", children }) });
250
275
  }
251
276
 
277
+ // src/auth/EmailDisclosure.tsx
278
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
279
+ function EmailDisclosure({ open, onToggle, controls, openLabel = "hide email sign-in", closedLabel = "or continue with email" }) {
280
+ return /* @__PURE__ */ jsx8("div", { className: "xc-or", "data-xc": "email-disclosure", children: /* @__PURE__ */ jsxs7("button", { type: "button", className: "xc-or__label xc-or__btn", onClick: onToggle, "aria-expanded": open, "aria-controls": controls, children: [
281
+ open ? openLabel : closedLabel,
282
+ /* @__PURE__ */ jsx8("svg", { className: "xc-or__caret", "data-open": open, viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx8("path", { d: "M4 6l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.5" }) })
283
+ ] }) });
284
+ }
285
+
252
286
  // src/auth/states.ts
253
287
  var STATES = [
254
288
  { id: "sign-in", props: { title: "Welcome back", body: "Sign in to your XENO account to use XENO Canvas.", actions: [{ id: "signIn", label: "Sign in to XENO", variant: "primary" }], note: "Your browser opens at xenostudio.ai, where you can continue with Google, GitHub or email. XENO Canvas never sees your password.", animate: false } },
@@ -265,6 +299,8 @@ var CREDENTIAL_STATES = [
265
299
  { id: "filled", props: { mode: "signin", values: { name: "", email: "you@example.com", password: "hunter22" }, title: "Welcome back", body: "Enter your credentials to access your account" } },
266
300
  { id: "errors", props: { mode: "signup", values: { name: "", email: "not-an-email", password: "123" }, errors: { name: "Please enter your full name", email: "Please enter a valid email address", password: "Password must be at least 6 characters" }, title: "Get started", body: "Create your account and start creating" } },
267
301
  { id: "submitting", props: { mode: "signin", values: { name: "", email: "you@example.com", password: "hunter22" }, submitting: true, title: "Welcome back", body: "Enter your credentials to access your account" } },
302
+ { id: "rules", props: { mode: "signup", values: { name: "Ada", email: "ada@example.com", password: "abcdefgh1" }, passwordRules: [{ label: "Minimum 8 letters", met: true }, { label: "At least one number", met: true }, { label: "At least one special character", met: false }], title: "Get started", body: "Create your account and start creating" } },
303
+ { id: "collapsed", props: { mode: "signin", values: empty, open: false, title: "Welcome back", body: "Enter your credentials to access your account" } },
268
304
  { id: "rejected", props: { mode: "signin", values: { name: "", email: "you@example.com", password: "hunter22" }, errors: { form: "Invalid email or password." }, title: "Welcome back", body: "Enter your credentials to access your account" } }
269
305
  ];
270
306
  export {
@@ -272,6 +308,7 @@ export {
272
308
  AuthHero,
273
309
  CREDENTIAL_STATES,
274
310
  CredentialForm,
311
+ EmailDisclosure,
275
312
  LicencePlate,
276
313
  OrDivider,
277
314
  STATES,
package/dist/index.d.ts CHANGED
@@ -3,6 +3,6 @@
3
3
  * Families are subpaths: `@xenosystem/components/auth`, `/navigation`, `/feedback`, `/data`.
4
4
  * This root re-exports nothing on purpose: import the family you use, so a bundle carries only it.
5
5
  */
6
- declare const COMPONENTS_VERSION = "0.2.1";
6
+ declare const COMPONENTS_VERSION = "0.3.0";
7
7
 
8
8
  export { COMPONENTS_VERSION };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- var COMPONENTS_VERSION = "0.2.1";
2
+ var COMPONENTS_VERSION = "0.3.0";
3
3
  export {
4
4
  COMPONENTS_VERSION
5
5
  };
package/package.json CHANGED
@@ -1,19 +1,29 @@
1
1
  {
2
2
  "name": "@xenosystem/components",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "XENO Components — rung 2 of the front ladder: molecules composed from @xenosystem/elements, with no host contract. Families as subpaths: /auth, /navigation, /feedback, /data.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
8
  "exports": {
9
- ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
10
- "./auth": { "types": "./dist/auth/index.d.ts", "import": "./dist/auth/index.js" }
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./auth": {
14
+ "types": "./dist/auth/index.d.ts",
15
+ "import": "./dist/auth/index.js"
16
+ }
11
17
  },
12
- "files": ["dist", "README.md"],
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
13
22
  "scripts": {
14
23
  "build": "tsup",
15
24
  "typecheck": "tsc --noEmit",
16
- "test": "vitest run"
25
+ "test": "vitest run",
26
+ "prepublishOnly": "node scripts/verify-dist.mjs"
17
27
  },
18
28
  "peerDependencies": {
19
29
  "@xenosystem/elements-react": ">=0.0.1",
@@ -31,5 +41,7 @@
31
41
  "typescript": "^5.4.0",
32
42
  "vitest": "^4.1.0"
33
43
  },
34
- "publishConfig": { "access": "public" }
44
+ "publishConfig": {
45
+ "access": "public"
46
+ }
35
47
  }