@ps1ui/core 0.0.0 → 0.0.1

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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/dist/base.css +378 -0
  3. package/dist/components/Anchor/Anchor.d.mts +12 -0
  4. package/dist/components/Anchor/Anchor.mjs +15 -0
  5. package/dist/components/Anchor/Anchor.mjs.map +1 -0
  6. package/dist/components/Button/Button.d.mts +24 -0
  7. package/dist/components/Button/Button.mjs +17 -0
  8. package/dist/components/Button/Button.mjs.map +1 -0
  9. package/dist/components/Card/Card.d.mts +7 -0
  10. package/dist/components/Card/Card.mjs +14 -0
  11. package/dist/components/Card/Card.mjs.map +1 -0
  12. package/dist/components/Checkbox/Checkbox.d.mts +9 -0
  13. package/dist/components/Checkbox/Checkbox.mjs +23 -0
  14. package/dist/components/Checkbox/Checkbox.mjs.map +1 -0
  15. package/dist/components/Code/Code.d.mts +7 -0
  16. package/dist/components/Code/Code.mjs +14 -0
  17. package/dist/components/Code/Code.mjs.map +1 -0
  18. package/dist/components/CodeBlock/CodeBlock.d.mts +12 -0
  19. package/dist/components/CodeBlock/CodeBlock.mjs +44 -0
  20. package/dist/components/CodeBlock/CodeBlock.mjs.map +1 -0
  21. package/dist/components/CodeBlock/refractor.d.mts +7 -0
  22. package/dist/components/CodeBlock/refractor.mjs +26 -0
  23. package/dist/components/CodeBlock/refractor.mjs.map +1 -0
  24. package/dist/components/Container/Container.d.mts +13 -0
  25. package/dist/components/Container/Container.mjs +24 -0
  26. package/dist/components/Container/Container.mjs.map +1 -0
  27. package/dist/components/Details/Details.d.mts +9 -0
  28. package/dist/components/Details/Details.mjs +20 -0
  29. package/dist/components/Details/Details.mjs.map +1 -0
  30. package/dist/components/Grid/Grid.d.mts +13 -0
  31. package/dist/components/Grid/Grid.mjs +24 -0
  32. package/dist/components/Grid/Grid.mjs.map +1 -0
  33. package/dist/components/GridItem/GridItem.d.mts +10 -0
  34. package/dist/components/GridItem/GridItem.mjs +21 -0
  35. package/dist/components/GridItem/GridItem.mjs.map +1 -0
  36. package/dist/components/Heading/Heading.d.mts +22 -0
  37. package/dist/components/Heading/Heading.mjs +53 -0
  38. package/dist/components/Heading/Heading.mjs.map +1 -0
  39. package/dist/components/Input/Input.d.mts +7 -0
  40. package/dist/components/Input/Input.mjs +15 -0
  41. package/dist/components/Input/Input.mjs.map +1 -0
  42. package/dist/components/Label/Label.d.mts +7 -0
  43. package/dist/components/Label/Label.mjs +14 -0
  44. package/dist/components/Label/Label.mjs.map +1 -0
  45. package/dist/components/List/List.d.mts +11 -0
  46. package/dist/components/List/List.mjs +15 -0
  47. package/dist/components/List/List.mjs.map +1 -0
  48. package/dist/components/ListItem/ListItem.d.mts +7 -0
  49. package/dist/components/ListItem/ListItem.mjs +13 -0
  50. package/dist/components/ListItem/ListItem.mjs.map +1 -0
  51. package/dist/components/PS1Root/PS1Root.d.mts +7 -0
  52. package/dist/components/PS1Root/PS1Root.mjs +13 -0
  53. package/dist/components/PS1Root/PS1Root.mjs.map +1 -0
  54. package/dist/components/Stack/Stack.d.mts +19 -0
  55. package/dist/components/Stack/Stack.mjs +48 -0
  56. package/dist/components/Stack/Stack.mjs.map +1 -0
  57. package/dist/components/Table/Table.d.mts +7 -0
  58. package/dist/components/Table/Table.mjs +25 -0
  59. package/dist/components/Table/Table.mjs.map +1 -0
  60. package/dist/components/Tbody/Tbody.d.mts +7 -0
  61. package/dist/components/Tbody/Tbody.mjs +13 -0
  62. package/dist/components/Tbody/Tbody.mjs.map +1 -0
  63. package/dist/components/Td/Td.d.mts +7 -0
  64. package/dist/components/Td/Td.mjs +13 -0
  65. package/dist/components/Td/Td.mjs.map +1 -0
  66. package/dist/components/Text/Text.d.mts +23 -0
  67. package/dist/components/Text/Text.mjs +34 -0
  68. package/dist/components/Text/Text.mjs.map +1 -0
  69. package/dist/components/Th/Th.d.mts +7 -0
  70. package/dist/components/Th/Th.mjs +13 -0
  71. package/dist/components/Th/Th.mjs.map +1 -0
  72. package/dist/components/Thead/Thead.d.mts +7 -0
  73. package/dist/components/Thead/Thead.mjs +13 -0
  74. package/dist/components/Thead/Thead.mjs.map +1 -0
  75. package/dist/components/Tr/Tr.d.mts +7 -0
  76. package/dist/components/Tr/Tr.mjs +13 -0
  77. package/dist/components/Tr/Tr.mjs.map +1 -0
  78. package/dist/components.css +1870 -0
  79. package/dist/index.d.mts +28 -0
  80. package/dist/index.mjs +25 -0
  81. package/dist/styles.css +1930 -0
  82. package/dist/utils/cx.mjs +8 -0
  83. package/dist/utils/cx.mjs.map +1 -0
  84. package/dist/utils/numbers.mjs +8 -0
  85. package/dist/utils/numbers.mjs.map +1 -0
  86. package/dist/utils/responsive.d.mts +6 -0
  87. package/dist/utils/responsive.mjs +39 -0
  88. package/dist/utils/responsive.mjs.map +1 -0
  89. package/dist/utils/spacing.d.mts +5 -0
  90. package/dist/utils/spacing.mjs +8 -0
  91. package/dist/utils/spacing.mjs.map +1 -0
  92. package/dist/utils/typography.d.mts +5 -0
  93. package/dist/utils/typography.mjs +16 -0
  94. package/dist/utils/typography.mjs.map +1 -0
  95. package/dist/utils/useMergedRef.mjs +30 -0
  96. package/dist/utils/useMergedRef.mjs.map +1 -0
  97. package/dist/utils/useScrollableFocus.mjs +29 -0
  98. package/dist/utils/useScrollableFocus.mjs.map +1 -0
  99. package/package.json +84 -3
@@ -0,0 +1,8 @@
1
+ //#region src/utils/cx.ts
2
+ function cx(...classes) {
3
+ return classes.filter(Boolean).join(" ");
4
+ }
5
+ //#endregion
6
+ export { cx };
7
+
8
+ //# sourceMappingURL=cx.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cx.mjs","names":[],"sources":["../../src/utils/cx.ts"],"sourcesContent":["export type ClassValue = string | false | null | undefined;\n\nexport function cx(...classes: ClassValue[]): string {\n return classes.filter(Boolean).join(\" \");\n}\n"],"mappings":";AAEA,SAAgB,GAAG,GAAG,SAA+B;CACnD,OAAO,QAAQ,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;AACzC"}
@@ -0,0 +1,8 @@
1
+ //#region src/utils/numbers.ts
2
+ function safePositiveInt(v) {
3
+ return Number.isFinite(v) ? Math.max(1, Math.floor(v)) : 1;
4
+ }
5
+ //#endregion
6
+ export { safePositiveInt };
7
+
8
+ //# sourceMappingURL=numbers.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"numbers.mjs","names":[],"sources":["../../src/utils/numbers.ts"],"sourcesContent":["// Clamp to a positive integer (≥ 1). Used at system boundaries where a\n// CSS property requires a positive-integer count and any other value drops\n// the whole declaration:\n// - `grid-template-columns: repeat(N, ...)` (Grid columns) — N ≥ 1 integer\n// - `grid-column: span N` (GridItem colSpan) — N ≥ 1 integer\n//\n// Guard here so a caller passing e.g. `items.length` on an empty list, or\n// `Number(badInput)`, still produces a valid 1-track value instead of the\n// browser silently dropping the declaration (which collapses the grid to\n// auto-flow / 1-track defaults — often the same visible result, but not\n// on negative / fractional inputs, and the debug story is worse either way).\n//\n// `Number.isFinite` gate is required because `Math.max(1, NaN)` is NaN and\n// `Math.max(1, Infinity)` is Infinity — neither hits the `Math.max(1, ...)`\n// clamp. Applied per-breakpoint entry when passed as a `resolveResponsive`\n// transform, so a responsive object like `{ base: NaN, md: 3 }` produces\n// `{ ${prefix}-base: 1, ${prefix}-md: 3 }`.\nexport function safePositiveInt(v: number): number {\n return Number.isFinite(v) ? Math.max(1, Math.floor(v)) : 1;\n}\n"],"mappings":";AAiBA,SAAgB,gBAAgB,GAAmB;CACjD,OAAO,OAAO,SAAS,CAAC,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,CAAC,CAAC,IAAI;AAC3D"}
@@ -0,0 +1,6 @@
1
+ //#region src/utils/responsive.d.ts
2
+ type Breakpoint = "base" | "sm" | "md" | "lg" | "xl";
3
+ type Responsive<T> = T | Partial<Record<Breakpoint, T>>;
4
+ //#endregion
5
+ export { Breakpoint, Responsive };
6
+ //# sourceMappingURL=responsive.d.mts.map
@@ -0,0 +1,39 @@
1
+ //#region src/utils/responsive.ts
2
+ const BREAKPOINTS = [
3
+ "base",
4
+ "sm",
5
+ "md",
6
+ "lg",
7
+ "xl"
8
+ ];
9
+ function isResponsiveObject(value) {
10
+ if (typeof value !== "object" || value === null) return false;
11
+ if (Array.isArray(value)) return false;
12
+ if ("$$typeof" in value) return false;
13
+ if (Object.getPrototypeOf(value) !== Object.prototype) return false;
14
+ const keys = Object.keys(value);
15
+ const bpSet = BREAKPOINTS;
16
+ return keys.every((k) => bpSet.includes(k));
17
+ }
18
+ function withResponsiveBase(value, defaultValue) {
19
+ if (value === void 0) return defaultValue;
20
+ if (isResponsiveObject(value) && value.base === void 0) return {
21
+ ...value,
22
+ base: defaultValue
23
+ };
24
+ return value;
25
+ }
26
+ function resolveResponsive(value, cssVarPrefix, transform) {
27
+ if (value === void 0) return {};
28
+ if (!isResponsiveObject(value)) return { [`${cssVarPrefix}-base`]: transform(value) };
29
+ const result = {};
30
+ for (const bp of BREAKPOINTS) {
31
+ const v = value[bp];
32
+ if (v !== void 0) result[`${cssVarPrefix}-${bp}`] = transform(v);
33
+ }
34
+ return result;
35
+ }
36
+ //#endregion
37
+ export { BREAKPOINTS, resolveResponsive, withResponsiveBase };
38
+
39
+ //# sourceMappingURL=responsive.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"responsive.mjs","names":[],"sources":["../../src/utils/responsive.ts"],"sourcesContent":["// Shared responsive prop primitives.\n//\n// A \"responsive value\" is either a scalar of type T, or a Partial record keyed\n// by breakpoint name. The five breakpoints are `base` / `sm` / `md` / `lg` /\n// `xl`, mobile-first: `base` is the value used at the narrowest container\n// width, and each wider breakpoint OVERRIDES the previous when its\n// `@container (min-width: X)` condition matches.\n//\n// Values are propagated to CSS via an inline-style cascade of CSS custom\n// properties: each responsive prop emits `${prefix}-${breakpoint}` custom\n// properties; the component's CSS then reads them inside `@container` blocks\n// with a fallback chain so a missing breakpoint inherits from the previous one.\n// See any responsive component's `.css` file for the concrete pattern.\n\nexport type Breakpoint = \"base\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n\n// Ordered narrowest → widest. The cascade in CSS reads this same order.\nexport const BREAKPOINTS = [\n \"base\",\n \"sm\",\n \"md\",\n \"lg\",\n \"xl\",\n] as const satisfies readonly Breakpoint[];\n\n// A responsive value: either a bare T, or a Partial map of breakpoint → T.\n// A bare T is equivalent to `{ base: T }` — a value that never varies.\nexport type Responsive<T> = T | Partial<Record<Breakpoint, T>>;\n\n// Distinguishes a Responsive object from a scalar. The definitive test is\n// (a) the value is a plain object literal (`Object.prototype` prototype —\n// gates out `Date` / `Map` / `Set` / `URL` / arrays / class instances, all\n// of which expose zero enumerable own keys and would otherwise pass the\n// vacuous `every` below), and (b) every enumerable own key is a known\n// breakpoint name. React elements (which carry `$$typeof`) are also\n// explicitly excluded — passing a React element as a scalar T is legal.\nfunction isResponsiveObject<T>(value: Responsive<T>): value is Partial<Record<Breakpoint, T>> {\n if (typeof value !== \"object\" || value === null) return false;\n if (Array.isArray(value)) return false;\n if (\"$$typeof\" in (value as object)) return false;\n // Plain object literal check: `Object.getPrototypeOf({}) === Object.prototype`,\n // but any host object (Date/Map/Set/URL, class instances) has a different\n // prototype and never qualifies as a Responsive shape — even when its\n // own enumerable keys happen to be empty (making the `every` check\n // vacuously true).\n if (Object.getPrototypeOf(value) !== Object.prototype) return false;\n const keys = Object.keys(value);\n // Empty object also qualifies — resolves to zero emitted custom\n // properties, letting the component's CSS default take over.\n const bpSet = BREAKPOINTS as readonly string[];\n return keys.every((k) => bpSet.includes(k));\n}\n\n// Ensure a Responsive<T> has a base value, using `defaultValue` when the\n// caller either omitted the prop entirely (undefined) or passed an object\n// without a `base` key. Scalars pass through untouched.\n//\n// Use this when a component's base-value default depends on another prop\n// (Heading's `size` default comes from `level` via LEVEL_DEFAULTS, for\n// example). Without this normalization, a caller writing\n// `<Heading level={1} size={{ md: \"2xl\" }}>` would land on the CSS fallback\n// at base (whatever the stylesheet picked as its own default) instead of\n// level 1's expected `3xl`.\nexport function withResponsiveBase<T>(\n value: Responsive<T> | undefined,\n defaultValue: T,\n): Responsive<T> {\n if (value === undefined) return defaultValue;\n if (isResponsiveObject<T>(value) && value.base === undefined) {\n // Spread first so an explicit `undefined` base in the caller's object\n // still gets overridden by the default (arguably surprising, but the\n // alternative would silently drop the base for the CSS fallback).\n return { ...value, base: defaultValue };\n }\n return value;\n}\n\n// Convert a Responsive<T> into a record of inline-style CSS custom properties.\n// - `undefined` → no properties (component's CSS default takes over)\n// - scalar T → `{ [`${prefix}-base`]: transform(value) }`\n// - Partial<Record<Breakpoint, T>> → one entry per defined breakpoint\n//\n// `transform` maps each T to the CSS value that will actually be assigned to\n// the custom property. For a `SpaceScale` gap prop this is typically\n// `v => v === \"none\" ? \"0\" : `var(--ps1ui-space-${v})``; for a numeric\n// `columns` prop, an identity + clamping wrapper. The transform is applied\n// per-breakpoint entry, not to the whole object.\nexport function resolveResponsive<T>(\n value: Responsive<T> | undefined,\n cssVarPrefix: string,\n transform: (v: T) => string | number,\n): Record<string, string | number> {\n if (value === undefined) return {};\n if (!isResponsiveObject<T>(value)) {\n return { [`${cssVarPrefix}-base`]: transform(value) };\n }\n const result: Record<string, string | number> = {};\n for (const bp of BREAKPOINTS) {\n const v = value[bp];\n if (v !== undefined) {\n result[`${cssVarPrefix}-${bp}`] = transform(v);\n }\n }\n return result;\n}\n"],"mappings":";AAiBA,MAAa,cAAc;CACzB;CACA;CACA;CACA;CACA;AACF;AAaA,SAAS,mBAAsB,OAA+D;CAC5F,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO;CACjC,IAAI,cAAe,OAAkB,OAAO;CAM5C,IAAI,OAAO,eAAe,KAAK,MAAM,OAAO,WAAW,OAAO;CAC9D,MAAM,OAAO,OAAO,KAAK,KAAK;CAG9B,MAAM,QAAQ;CACd,OAAO,KAAK,OAAO,MAAM,MAAM,SAAS,CAAC,CAAC;AAC5C;AAYA,SAAgB,mBACd,OACA,cACe;CACf,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,IAAI,mBAAsB,KAAK,KAAK,MAAM,SAAS,KAAA,GAIjD,OAAO;EAAE,GAAG;EAAO,MAAM;CAAa;CAExC,OAAO;AACT;AAYA,SAAgB,kBACd,OACA,cACA,WACiC;CACjC,IAAI,UAAU,KAAA,GAAW,OAAO,CAAC;CACjC,IAAI,CAAC,mBAAsB,KAAK,GAC9B,OAAO,GAAG,GAAG,aAAa,SAAS,UAAU,KAAK,EAAE;CAEtD,MAAM,SAA0C,CAAC;CACjD,KAAK,MAAM,MAAM,aAAa;EAC5B,MAAM,IAAI,MAAM;EAChB,IAAI,MAAM,KAAA,GACR,OAAO,GAAG,aAAa,GAAG,QAAQ,UAAU,CAAC;CAEjD;CACA,OAAO;AACT"}
@@ -0,0 +1,5 @@
1
+ //#region src/utils/spacing.d.ts
2
+ type SpaceScale = "none" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
3
+ //#endregion
4
+ export { SpaceScale };
5
+ //# sourceMappingURL=spacing.d.mts.map
@@ -0,0 +1,8 @@
1
+ //#region src/utils/spacing.ts
2
+ function spaceToVar(v) {
3
+ return v === "none" ? "0" : `var(--ps1ui-space-${v})`;
4
+ }
5
+ //#endregion
6
+ export { spaceToVar };
7
+
8
+ //# sourceMappingURL=spacing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spacing.mjs","names":[],"sources":["../../src/utils/spacing.ts"],"sourcesContent":["// Shared spacing scale for layout primitives (Stack `gap`, Grid `gap`,\n// Container `px`, and any future primitive that consumes a horizontal /\n// vertical spacing token). Mirrors the `--ps1ui-space-*` tokens in\n// `src/styles/tokens.css` one-to-one, with the `none` sentinel for 0.\n//\n// A single shared type prevents drift across primitives — a future scale\n// addition (e.g. `3xl`) touches this file once; each consumer's `gap` /\n// `px` prop picks it up for free.\nexport type SpaceScale = \"none\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\";\n\n// SpaceScale → CSS value. `none` is `0` (unitless — pair with any length\n// context); every other value is a `var(...)` reference to the matching\n// `--ps1ui-space-*` token so a token rename cascades naturally.\n//\n// Kept alongside `SpaceScale` on purpose: the mapping is 1:1 with the union,\n// so a new scale value forces a co-located update instead of drifting away\n// in a component's private helper. Every layout primitive that maps a\n// SpaceScale prop to inline CSS custom-property values calls this — Container\n// (`px`), Grid (`gap`), Stack (`gap`) — so any future rename or sentinel\n// change lands in exactly one place.\nexport function spaceToVar(v: SpaceScale): string {\n return v === \"none\" ? \"0\" : `var(--ps1ui-space-${v})`;\n}\n"],"mappings":";AAoBA,SAAgB,WAAW,GAAuB;CAChD,OAAO,MAAM,SAAS,MAAM,qBAAqB,EAAE;AACrD"}
@@ -0,0 +1,5 @@
1
+ //#region src/utils/typography.d.ts
2
+ type FontWeight = "regular" | "medium" | "semibold" | "bold";
3
+ //#endregion
4
+ export { FontWeight };
5
+ //# sourceMappingURL=typography.d.mts.map
@@ -0,0 +1,16 @@
1
+ //#region src/utils/typography.ts
2
+ function fontSizeToVar(v) {
3
+ return `var(--ps1ui-font-size-${v})`;
4
+ }
5
+ function weightToValue(v) {
6
+ switch (v) {
7
+ case "regular": return 400;
8
+ case "medium": return 500;
9
+ case "semibold": return 600;
10
+ case "bold": return 700;
11
+ }
12
+ }
13
+ //#endregion
14
+ export { fontSizeToVar, weightToValue };
15
+
16
+ //# sourceMappingURL=typography.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typography.mjs","names":[],"sources":["../../src/utils/typography.ts"],"sourcesContent":["// Shared typography primitives for components that expose a `size` or\n// `weight` prop backed by --ps1ui-font-size-* / a discrete numeric\n// font-weight scale. Currently consumed by Text and Heading.\n//\n// A shared surface prevents the two components from silently drifting on\n// what \"medium\" or \"sm\" means; add a new weight or size once here and both\n// components pick it up when they extend their own local unions.\n\n// FontSizeToken is the raw slug shared across every ps1ui typography scale\n// (Text: \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"; Heading: \"sm\" | \"md\" | \"lg\" |\n// \"xl\" | \"2xl\" | \"3xl\"). Each component narrows the union in its own\n// component-local size type; `fontSizeToVar` accepts the widest union so it\n// can serve both without a cast.\nexport type FontSizeToken = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\";\n\n// FontWeight is the shared weight scale — every ps1ui typography component\n// maps its `weight` prop through this union.\nexport type FontWeight = \"regular\" | \"medium\" | \"semibold\" | \"bold\";\n\n// FontSizeToken → CSS `var(--ps1ui-font-size-<slug>)`. Kept as a single\n// mapping function so a token rename lands in one place. Text and Heading\n// both call this with their own narrower slug union.\nexport function fontSizeToVar<S extends FontSizeToken>(v: S): string {\n return `var(--ps1ui-font-size-${v})`;\n}\n\n// FontWeight → numeric CSS font-weight. Discrete 100-step scale that keeps\n// author declarations (author > UA) in charge: `regular` = 400 (normal),\n// `medium` = 500, `semibold` = 600, `bold` = 700. If a future consumer\n// wants an even lighter or heavier step, add the union member here and the\n// mapping in the same commit.\nexport function weightToValue(v: FontWeight): number {\n switch (v) {\n case \"regular\":\n return 400;\n case \"medium\":\n return 500;\n case \"semibold\":\n return 600;\n case \"bold\":\n return 700;\n }\n}\n"],"mappings":";AAsBA,SAAgB,cAAuC,GAAc;CACnE,OAAO,yBAAyB,EAAE;AACpC;AAOA,SAAgB,cAAc,GAAuB;CACnD,QAAQ,GAAR;EACE,KAAK,WACH,OAAO;EACT,KAAK,UACH,OAAO;EACT,KAAK,YACH,OAAO;EACT,KAAK,QACH,OAAO;CACX;AACF"}
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { useCallback } from "react";
3
+ //#region src/utils/useMergedRef.ts
4
+ function attach(ref, node) {
5
+ if (typeof ref === "function") {
6
+ const cleanup = ref(node);
7
+ return typeof cleanup === "function" ? cleanup : () => ref(null);
8
+ }
9
+ if (ref != null) {
10
+ ref.current = node;
11
+ return () => {
12
+ ref.current = null;
13
+ };
14
+ }
15
+ return () => {};
16
+ }
17
+ function useMergedRef(a, b) {
18
+ return useCallback((node) => {
19
+ const detachA = attach(a, node);
20
+ const detachB = attach(b, node);
21
+ return () => {
22
+ detachA();
23
+ detachB();
24
+ };
25
+ }, [a, b]);
26
+ }
27
+ //#endregion
28
+ export { useMergedRef };
29
+
30
+ //# sourceMappingURL=useMergedRef.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMergedRef.mjs","names":[],"sources":["../../src/utils/useMergedRef.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback } from \"react\";\nimport type { Ref, RefCallback } from \"react\";\n\n// Attach one ref and return the matching detach step. React 19 callback refs\n// may return their own cleanup — when they do, honoring it (instead of the\n// legacy call-with-null) is part of the ref contract: a caller that returns a\n// cleanup writes `(node) => { …use node…; return () => {…}; }` and its body is\n// never written to tolerate `node === null`.\nfunction attach<T>(ref: Ref<T> | undefined, node: T | null): () => void {\n if (typeof ref === \"function\") {\n const cleanup = ref(node);\n return typeof cleanup === \"function\" ? cleanup : () => ref(null);\n }\n if (ref != null) {\n ref.current = node;\n return () => {\n ref.current = null;\n };\n }\n return () => {};\n}\n\n// Merge an internal ref with the caller's forwarded ref so both receive the\n// DOM node (Checkbox / CodeBlock / Table all need an internal handle next to\n// the public one). The returned callback itself returns a cleanup, so React 19\n// drives detachment through it — each merged ref is detached the way it was\n// attached (its own cleanup, call-with-null, or `current = null`).\nexport function useMergedRef<T>(a: Ref<T> | undefined, b: Ref<T> | undefined): RefCallback<T> {\n return useCallback(\n (node: T | null) => {\n const detachA = attach(a, node);\n const detachB = attach(b, node);\n return () => {\n detachA();\n detachB();\n };\n },\n [a, b],\n );\n}\n"],"mappings":";;;AAUA,SAAS,OAAU,KAAyB,MAA4B;CACtE,IAAI,OAAO,QAAQ,YAAY;EAC7B,MAAM,UAAU,IAAI,IAAI;EACxB,OAAO,OAAO,YAAY,aAAa,gBAAgB,IAAI,IAAI;CACjE;CACA,IAAI,OAAO,MAAM;EACf,IAAI,UAAU;EACd,aAAa;GACX,IAAI,UAAU;EAChB;CACF;CACA,aAAa,CAAC;AAChB;AAOA,SAAgB,aAAgB,GAAuB,GAAuC;CAC5F,OAAO,aACJ,SAAmB;EAClB,MAAM,UAAU,OAAO,GAAG,IAAI;EAC9B,MAAM,UAAU,OAAO,GAAG,IAAI;EAC9B,aAAa;GACX,QAAQ;GACR,QAAQ;EACV;CACF,GACA,CAAC,GAAG,CAAC,CACP;AACF"}
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { useCallback, useLayoutEffect, useRef, useState } from "react";
3
+ //#region src/utils/useScrollableFocus.ts
4
+ function useScrollableFocus() {
5
+ const scrollerRef = useRef(null);
6
+ const contentRef = useRef(null);
7
+ const [scrollable, setScrollable] = useState(true);
8
+ const measure = useCallback(() => {
9
+ const el = scrollerRef.current;
10
+ setScrollable(el.scrollWidth > el.clientWidth || document.activeElement === el);
11
+ }, []);
12
+ useLayoutEffect(() => {
13
+ measure();
14
+ const ro = new ResizeObserver(measure);
15
+ ro.observe(scrollerRef.current);
16
+ if (contentRef.current) ro.observe(contentRef.current);
17
+ return () => ro.disconnect();
18
+ }, [measure]);
19
+ return {
20
+ scrollerRef,
21
+ contentRef,
22
+ tabIndex: scrollable ? 0 : void 0,
23
+ measure
24
+ };
25
+ }
26
+ //#endregion
27
+ export { useScrollableFocus };
28
+
29
+ //# sourceMappingURL=useScrollableFocus.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useScrollableFocus.mjs","names":[],"sources":["../../src/utils/useScrollableFocus.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useLayoutEffect, useRef, useState } from \"react\";\nimport type { RefObject } from \"react\";\n\n// Measured keyboard reachability for a horizontal-overflow scroll container,\n// shared by CodeBlock (the <pre> is its own scroller) and Table (a wrapper div\n// scrolls the <table>).\n//\n// axe scrollable-region-focusable requires a scrollable region to be\n// keyboard-reachable IF its content actually overflows, while an unconditional\n// tabIndex=0 would drop every non-overflowing instance into the tab order for\n// no benefit. So the hook measures and exposes `tabIndex: 0` only while\n// there is somewhere to scroll.\n//\n// The initial state is scrollable (`true`) so overflowing content is never\n// *un*reachable on the very first paint, before useLayoutEffect has measured.\n// The same bias is deliberate for static SSR without hydration (e.g. the docs\n// site renders every component server-side with no client JS): the measurement\n// never runs there, and whether a table overflows depends on the reader's\n// viewport — a phone-width visitor needs the scroller reachable, so the safe\n// static default is an extra tab stop on wide screens, not unreachable content\n// on narrow ones.\n//\n// Two more contract details, both load-bearing:\n// - `measure` keeps the scroller in the tab order while it IS the focused\n// element even if its content now fits — removing tabindex from the\n// focused element blurs it to <body>, silently teleporting a keyboard\n// user's position. Components re-run `measure` on the scroller's blur so\n// the tab stop is dropped once focus has moved on.\n// - The quantity measured (scrollWidth) never fires a ResizeObserver by\n// itself, so the hook observes the scroller's box (container resizes) and,\n// when provided, a content box via `contentRef` (content-driven growth —\n// Table observes its <table>). Content whose box can't be observed (inline\n// elements report no box) needs a manual `measure()` keyed on the content\n// instead — CodeBlock re-measures keyed on its source string.\nexport function useScrollableFocus<S extends HTMLElement, C extends HTMLElement = HTMLElement>(): {\n scrollerRef: RefObject<S | null>;\n contentRef: RefObject<C | null>;\n tabIndex: 0 | undefined;\n measure: () => void;\n} {\n const scrollerRef = useRef<S | null>(null);\n const contentRef = useRef<C | null>(null);\n const [scrollable, setScrollable] = useState(true);\n\n const measure = useCallback(() => {\n // scrollerRef is populated before layout effects fire (React sets refs\n // during commit) and every later call site holds a live element, so no\n // defensive null check.\n const el = scrollerRef.current!;\n setScrollable(el.scrollWidth > el.clientWidth || document.activeElement === el);\n }, []);\n\n useLayoutEffect(() => {\n measure();\n const ro = new ResizeObserver(measure);\n ro.observe(scrollerRef.current!);\n if (contentRef.current) ro.observe(contentRef.current);\n return () => ro.disconnect();\n }, [measure]);\n\n return { scrollerRef, contentRef, tabIndex: scrollable ? 0 : undefined, measure };\n}\n"],"mappings":";;;AAoCA,SAAgB,qBAKd;CACA,MAAM,cAAc,OAAiB,IAAI;CACzC,MAAM,aAAa,OAAiB,IAAI;CACxC,MAAM,CAAC,YAAY,iBAAiB,SAAS,IAAI;CAEjD,MAAM,UAAU,kBAAkB;EAIhC,MAAM,KAAK,YAAY;EACvB,cAAc,GAAG,cAAc,GAAG,eAAe,SAAS,kBAAkB,EAAE;CAChF,GAAG,CAAC,CAAC;CAEL,sBAAsB;EACpB,QAAQ;EACR,MAAM,KAAK,IAAI,eAAe,OAAO;EACrC,GAAG,QAAQ,YAAY,OAAQ;EAC/B,IAAI,WAAW,SAAS,GAAG,QAAQ,WAAW,OAAO;EACrD,aAAa,GAAG,WAAW;CAC7B,GAAG,CAAC,OAAO,CAAC;CAEZ,OAAO;EAAE;EAAa;EAAY,UAAU,aAAa,IAAI,KAAA;EAAW;CAAQ;AAClF"}
package/package.json CHANGED
@@ -1,11 +1,92 @@
1
1
  {
2
2
  "name": "@ps1ui/core",
3
- "version": "0.0.0",
4
- "author": "koki-develop <kou.pg.0131@gmail.com>",
3
+ "version": "0.0.1",
5
4
  "homepage": "https://github.com/koki-develop/ps1ui",
5
+ "license": "MIT",
6
+ "author": "koki-develop <kou.pg.0131@gmail.com>",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "git+https://github.com/koki-develop/ps1ui.git"
9
10
  },
10
- "license": "MIT"
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "type": "module",
15
+ "sideEffects": [
16
+ "**/*.css",
17
+ "**/CodeBlock/refractor.*"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.mts",
22
+ "default": "./dist/index.mjs"
23
+ },
24
+ "./styles.css": "./dist/styles.css",
25
+ "./base.css": "./dist/base.css",
26
+ "./components.css": "./dist/components.css",
27
+ "./package.json": "./package.json"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "prepublishOnly": "pnpm build",
34
+ "build": "pnpm check && pnpm build:js && pnpm build:css && pnpm check:dist",
35
+ "build:js": "tsdown",
36
+ "build:css": "postcss src/styles/styles.css src/styles/base.css src/styles/components.css --dir dist/",
37
+ "lint": "oxlint",
38
+ "lint:fix": "oxlint --fix",
39
+ "fmt": "oxfmt",
40
+ "fmt:check": "oxfmt --check",
41
+ "check": "pnpm typecheck && pnpm check:palette && pnpm check:languages && pnpm check:responsive-property-coverage",
42
+ "check:palette": "node scripts/check-palette-sync.mjs",
43
+ "check:languages": "node scripts/check-languages.mjs",
44
+ "check:responsive-property-coverage": "node scripts/check-responsive-property-coverage.mjs",
45
+ "check:dist": "node scripts/check-dist-registrations.mjs && node scripts/check-css-split.mjs",
46
+ "typecheck": "tsc --noEmit",
47
+ "test": "vitest",
48
+ "test:ci": "vitest run --project unit --project storybook",
49
+ "test:coverage": "vitest run --coverage --project unit --project storybook",
50
+ "test:vrt": "vitest run --project vrt",
51
+ "test:vrt:update": "vitest run --project vrt -u",
52
+ "storybook": "storybook dev -p 6006",
53
+ "build-storybook": "storybook build"
54
+ },
55
+ "dependencies": {
56
+ "@fontsource-variable/jetbrains-mono": "^5.2.8",
57
+ "hast-util-to-jsx-runtime": "2.3.6",
58
+ "refractor": "5.0.0"
59
+ },
60
+ "devDependencies": {
61
+ "@storybook/addon-a11y": "10.5.0",
62
+ "@storybook/addon-docs": "10.5.0",
63
+ "@storybook/addon-vitest": "10.5.0",
64
+ "@storybook/react-vite": "10.5.0",
65
+ "@types/react": "19.2.17",
66
+ "@types/react-dom": "19.2.3",
67
+ "@vitejs/plugin-react": "6.0.3",
68
+ "@vitest/browser": "4.1.10",
69
+ "@vitest/browser-playwright": "4.1.10",
70
+ "@vitest/coverage-v8": "4.1.10",
71
+ "autoprefixer": "10.5.2",
72
+ "axe-core": "4.12.1",
73
+ "oxfmt": "0.58.0",
74
+ "oxlint": "1.73.0",
75
+ "playwright": "1.61.1",
76
+ "postcss": "8.5.16",
77
+ "postcss-cli": "11.0.1",
78
+ "postcss-import": "16.1.1",
79
+ "react": "19.2.7",
80
+ "react-dom": "19.2.7",
81
+ "storybook": "10.5.0",
82
+ "tsdown": "0.22.4",
83
+ "typescript": "7.0.2",
84
+ "vite": "8.1.4",
85
+ "vitest": "4.1.10",
86
+ "vitest-browser-react": "2.2.0"
87
+ },
88
+ "peerDependencies": {
89
+ "react": "^19.0.0",
90
+ "react-dom": "^19.0.0"
91
+ }
11
92
  }