@carissa-io/components-list 0.0.0-stage → 0.1.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
@@ -1,3 +1,75 @@
1
- # Temporary Holding Version
1
+ # @carissa-io/components-list
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ List: `List` and `ListItem`. A `ul` or `ol` of rows with leading and trailing slots, secondary text, link and button rows and
4
+ three variants. A React component set, SCSS-themed, with no other dependency.
5
+
6
+ ```bash
7
+ npm i @carissa-io/components-list
8
+ ```
9
+
10
+ ## Use
11
+
12
+ ```jsx
13
+ import { List, ListItem } from '@carissa-io/components-list'
14
+
15
+ <List variant="divided" aria-label={t('recent_files')}>
16
+ <ListItem leading={<FileIcon />} description={t('edited_today')} trailing={<Badge>3</Badge>} href="/files/1" selected>
17
+ Report.pdf
18
+ </ListItem>
19
+ <ListItem leading={<FileIcon />} interactive onClick={open}>
20
+ Notes.txt
21
+ </ListItem>
22
+ <ListItem>Read-only row</ListItem>
23
+ </List>
24
+ ```
25
+
26
+ | Part | Prop | |
27
+ |---|---|---|
28
+ | `List` | `as` | `ul` (default) or `ol`; anything else falls back to `ul` |
29
+ | | `variant` | `plain` (default), `bordered` (outer border) or `divided` (a divider between items); anything else falls back to `plain` |
30
+ | | `size` | `sm`, `md` (default) or `lg`, the row padding and font size; anything else falls back to `md` |
31
+ | `ListItem` | `leading`, `trailing` | slots before and after the text (an icon, an avatar, an action, a value) |
32
+ | | `description` | secondary text under the main text |
33
+ | | `href` | renders a link row (`<a>`) |
34
+ | | `interactive` | without `href`, renders a `button` row; `onClick` etc. go to it |
35
+ | | `selected` | `aria-current="true"` on the row element and the `c-list__item--selected` class |
36
+ | | `disabled` | disables a button row; a link row gets `aria-disabled="true"` and loses its `href` |
37
+ | | `as` | the item's own tag: `li` (default) or `div`; anything else falls back to `li` |
38
+
39
+ `className` is appended on `List` and to the `li` of an item; unknown props go to the list on `List` and to the row element
40
+ (link, button or div) on `ListItem`. `href` wins over `interactive`.
41
+
42
+ ## Accessibility
43
+
44
+ A real list (`ul`/`ol` with `li`), so screen readers announce the number of items. Link and button rows are native
45
+ elements: Tab reaches them, Enter follows a link, Enter and Space press a button. A plain row is not a tab stop. `selected` uses
46
+ `aria-current`, which suits navigation and "current item" lists; for a multi-select list the app puts its own checkbox in the
47
+ `leading` slot. Give the list an accessible name (`aria-label`) when it is not introduced by a heading.
48
+
49
+ ## Style
50
+
51
+ ```scss
52
+ @use '@carissa-io/components-list/scss' with (
53
+ $c-list-item-padding-x: 1rem,
54
+ $c-list-border-color: #ddd
55
+ );
56
+ ```
57
+
58
+ Classes: `c-list` (`--plain`, `--bordered`, `--divided`, `--sm`, `--md`, `--lg`), `c-list__item` (`--interactive`,
59
+ `--selected`, `--disabled`), `c-list__row`, `c-list__leading`, `c-list__body`, `c-list__text`, `c-list__description` and
60
+ `c-list__trailing`. Every variable is `!default` (see [scss/_variables.scss](scss/_variables.scss)); the defaults are neutral, so
61
+ the component works without a theme. Transitions are disabled with `prefers-reduced-motion`.
62
+
63
+ | Variable | Default |
64
+ |---|---|
65
+ | `$c-list-item-padding-x`, `$c-list-item-padding-y` | `0.75rem`, `0.5rem` |
66
+ | `$c-list-item-padding-y-sm`, `$c-list-item-padding-y-lg` | `0.25rem`, `0.75rem` |
67
+ | `$c-list-item-gap` | `0.75rem` |
68
+ | `$c-list-font-size`, `$c-list-font-size-sm`, `$c-list-font-size-lg` | `1rem`, `0.875rem`, `1.125rem` |
69
+ | `$c-list-ordered-indent` | `1.5rem` |
70
+ | `$c-list-description-color`, `$c-list-description-font-size` | `rgb(0 0 0 / 0.6)`, `0.875rem` |
71
+ | `$c-list-border-width`, `$c-list-border-color`, `$c-list-radius` | `1px`, `rgb(0 0 0 / 0.15)`, `0.375rem` |
72
+ | `$c-list-item-background-hover`, `$c-list-item-background-selected` | `rgb(0 0 0 / 0.04)`, `rgb(0 0 0 / 0.08)` |
73
+ | `$c-list-item-color-selected`, `$c-list-item-disabled-opacity` | `inherit`, `0.5` |
74
+ | `$c-list-focus-outline`, `$c-list-focus-outline-offset` | `2px solid currentColor`, `-2px` |
75
+ | `$c-list-transition-duration` | `0.15s` |
package/dist/index.js ADDED
@@ -0,0 +1,68 @@
1
+ // src/List.jsx
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ var TAGS = ["ul", "ol"];
4
+ var VARIANTS = ["plain", "bordered", "divided"];
5
+ var SIZES = ["sm", "md", "lg"];
6
+ var ITEM_TAGS = ["li", "div"];
7
+ var has = (node) => node !== void 0 && node !== null && node !== false && node !== "";
8
+ var join = (...parts) => parts.filter(Boolean).join(" ");
9
+ function List({ as = "ul", variant = "plain", size = "md", className = "", children, ...rest }) {
10
+ const Tag = TAGS.includes(as) ? as : "ul";
11
+ const safe_variant = VARIANTS.includes(variant) ? variant : "plain";
12
+ const safe_size = SIZES.includes(size) ? size : "md";
13
+ return /* @__PURE__ */ jsx(Tag, { ...rest, className: join("c-list", `c-list--${safe_variant}`, `c-list--${safe_size}`, className), children });
14
+ }
15
+ function ListItem({
16
+ leading,
17
+ trailing,
18
+ description,
19
+ interactive = false,
20
+ href,
21
+ as = "li",
22
+ selected = false,
23
+ disabled = false,
24
+ className = "",
25
+ children,
26
+ ...rest
27
+ }) {
28
+ const Tag = ITEM_TAGS.includes(as) ? as : "li";
29
+ const is_link = has(href);
30
+ const is_button = !is_link && interactive === true;
31
+ const is_selected = selected === true;
32
+ const is_disabled = disabled === true;
33
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
34
+ has(leading) ? /* @__PURE__ */ jsx("span", { className: "c-list__leading", children: leading }) : null,
35
+ /* @__PURE__ */ jsxs("span", { className: "c-list__body", children: [
36
+ /* @__PURE__ */ jsx("span", { className: "c-list__text", children }),
37
+ has(description) ? /* @__PURE__ */ jsx("span", { className: "c-list__description", children: description }) : null
38
+ ] }),
39
+ has(trailing) ? /* @__PURE__ */ jsx("span", { className: "c-list__trailing", children: trailing }) : null
40
+ ] });
41
+ const row_class = "c-list__row";
42
+ const row_props = { ...rest, "aria-current": is_selected ? "true" : void 0 };
43
+ let row;
44
+ if (is_link) {
45
+ row = is_disabled ? /* @__PURE__ */ jsx("a", { ...row_props, role: "link", "aria-disabled": "true", className: row_class, children: content }) : /* @__PURE__ */ jsx("a", { ...row_props, href, className: row_class, children: content });
46
+ } else if (is_button) {
47
+ row = /* @__PURE__ */ jsx("button", { type: "button", ...row_props, disabled: is_disabled, className: row_class, children: content });
48
+ } else {
49
+ row = /* @__PURE__ */ jsx("div", { ...row_props, className: row_class, children: content });
50
+ }
51
+ return /* @__PURE__ */ jsx(
52
+ Tag,
53
+ {
54
+ className: join(
55
+ "c-list__item",
56
+ (is_link || is_button) && "c-list__item--interactive",
57
+ is_selected && "c-list__item--selected",
58
+ is_disabled && "c-list__item--disabled",
59
+ className
60
+ ),
61
+ children: row
62
+ }
63
+ );
64
+ }
65
+ export {
66
+ List,
67
+ ListItem
68
+ };
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "@carissa-io/components-list",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Carissa UI: list with leading and trailing slots, link rows and variants as React components with SCSS variables",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "files": [
8
+ "dist",
9
+ "scss",
10
+ "THIRD-PARTY-NOTICES"
11
+ ],
12
+ "exports": {
13
+ ".": "./dist/index.js",
14
+ "./scss": "./scss/index.scss",
15
+ "./scss/*": "./scss/*"
16
+ },
17
+ "sideEffects": false,
18
+ "scripts": {
19
+ "build": "esbuild src/index.js --bundle --packages=external --format=esm --jsx=automatic --outfile=dist/index.js",
20
+ "test": "vitest run",
21
+ "lint": "eslint .",
22
+ "prepublishOnly": "node -e \"if (!process.env.GITHUB_ACTIONS) { console.error('Refusing to publish outside GitHub Actions CI: commit and push to main instead.'); process.exit(1); }\" && npm run build"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/carissa-io/components-list.git"
27
+ },
28
+ "license": "UNLICENSED",
29
+ "engines": {
30
+ "node": ">=24"
31
+ },
32
+ "peerDependencies": {
33
+ "react": ">=18"
34
+ },
35
+ "devDependencies": {
36
+ "@eslint/js": "^10.0.1",
37
+ "@testing-library/jest-dom": "^7.0.1",
38
+ "@testing-library/react": "^16.3.3",
39
+ "@testing-library/user-event": "^14.6.7",
40
+ "@vitejs/plugin-react": "^6.1.1",
41
+ "esbuild": "^0.28.0",
42
+ "eslint": "^10.12.0",
43
+ "eslint-plugin-react-hooks": "^7.1.1",
44
+ "globals": "^17.13.0",
45
+ "jsdom": "^29.1.1",
46
+ "react": "^19.3.0",
47
+ "react-dom": "^19.3.0",
48
+ "sass-embedded": "^1.105.1",
49
+ "vite": "^8.3.2",
50
+ "vitest": "^4.1.11"
51
+ },
52
+ "publishConfig": {
53
+ "registry": "https://registry.npmjs.org",
54
+ "access": "public"
55
+ }
56
+ }
@@ -0,0 +1,115 @@
1
+ @use 'variables' as *;
2
+
3
+ .c-list {
4
+ box-sizing: border-box;
5
+ padding: 0;
6
+ margin: 0;
7
+ font-size: $c-list-font-size;
8
+ list-style: none;
9
+ }
10
+
11
+ ol.c-list {
12
+ padding-inline-start: $c-list-ordered-indent;
13
+ list-style: decimal;
14
+ }
15
+
16
+ .c-list--sm {
17
+ font-size: $c-list-font-size-sm;
18
+ }
19
+
20
+ .c-list--lg {
21
+ font-size: $c-list-font-size-lg;
22
+ }
23
+
24
+ .c-list--bordered {
25
+ overflow: hidden;
26
+ border: $c-list-border-width solid $c-list-border-color;
27
+ border-radius: $c-list-radius;
28
+ }
29
+
30
+ .c-list--bordered > .c-list__item + .c-list__item,
31
+ .c-list--divided > .c-list__item + .c-list__item {
32
+ border-top: $c-list-border-width solid $c-list-border-color;
33
+ }
34
+
35
+ .c-list__item {
36
+ display: list-item;
37
+ padding: 0;
38
+ margin: 0;
39
+
40
+ &--selected {
41
+ color: $c-list-item-color-selected;
42
+ background: $c-list-item-background-selected;
43
+ }
44
+
45
+ &--disabled {
46
+ cursor: not-allowed;
47
+ opacity: $c-list-item-disabled-opacity;
48
+ }
49
+ }
50
+
51
+ .c-list__row {
52
+ box-sizing: border-box;
53
+ display: flex;
54
+ gap: $c-list-item-gap;
55
+ align-items: center;
56
+ width: 100%;
57
+ padding: $c-list-item-padding-y $c-list-item-padding-x;
58
+ font: inherit;
59
+ color: inherit;
60
+ text-align: start;
61
+ text-decoration: none;
62
+ background: transparent;
63
+ border: 0;
64
+ }
65
+
66
+ .c-list--sm .c-list__row {
67
+ padding-block: $c-list-item-padding-y-sm;
68
+ }
69
+
70
+ .c-list--lg .c-list__row {
71
+ padding-block: $c-list-item-padding-y-lg;
72
+ }
73
+
74
+ .c-list__item--interactive > .c-list__row {
75
+ cursor: pointer;
76
+ transition: background-color $c-list-transition-duration ease-out;
77
+
78
+ &:hover {
79
+ background: $c-list-item-background-hover;
80
+ }
81
+
82
+ &:focus-visible {
83
+ outline: $c-list-focus-outline;
84
+ outline-offset: $c-list-focus-outline-offset;
85
+ }
86
+ }
87
+
88
+ .c-list__item--disabled > .c-list__row {
89
+ pointer-events: none;
90
+ }
91
+
92
+ .c-list__leading,
93
+ .c-list__trailing {
94
+ display: inline-flex;
95
+ flex: none;
96
+ align-items: center;
97
+ }
98
+
99
+ .c-list__body {
100
+ display: flex;
101
+ flex: 1;
102
+ flex-direction: column;
103
+ min-width: 0;
104
+ }
105
+
106
+ .c-list__description {
107
+ font-size: $c-list-description-font-size;
108
+ color: $c-list-description-color;
109
+ }
110
+
111
+ @media (prefers-reduced-motion: reduce) {
112
+ .c-list__item--interactive > .c-list__row {
113
+ transition: none;
114
+ }
115
+ }
@@ -0,0 +1,38 @@
1
+ // Every value is `!default`: set it before the first @use / through `with (...)` and it wins.
2
+ //
3
+ // @use '@carissa-io/components-list/scss' with ($c-list-item-padding-x: 1rem, $c-list-border-color: #ddd);
4
+ //
5
+ // The defaults are neutral (no fills, thin borders), so the component works without a theme.
6
+
7
+ // Item
8
+ $c-list-item-padding-x: 0.75rem !default;
9
+ $c-list-item-padding-y: 0.5rem !default;
10
+ $c-list-item-padding-y-sm: 0.25rem !default;
11
+ $c-list-item-padding-y-lg: 0.75rem !default;
12
+ $c-list-item-gap: 0.75rem !default; // between leading, body and trailing
13
+ $c-list-font-size: 1rem !default;
14
+ $c-list-font-size-sm: 0.875rem !default;
15
+ $c-list-font-size-lg: 1.125rem !default;
16
+ $c-list-ordered-indent: 1.5rem !default; // the number column of an `ol`
17
+
18
+ // Secondary text
19
+ $c-list-description-color: rgb(0 0 0 / 0.6) !default;
20
+ $c-list-description-font-size: 0.875rem !default;
21
+
22
+ // Borders and variants
23
+ $c-list-border-width: 1px !default;
24
+ $c-list-border-color: rgb(0 0 0 / 0.15) !default;
25
+ $c-list-radius: 0.375rem !default;
26
+
27
+ // States
28
+ $c-list-item-background-hover: rgb(0 0 0 / 0.04) !default;
29
+ $c-list-item-background-selected: rgb(0 0 0 / 0.08) !default;
30
+ $c-list-item-color-selected: inherit !default;
31
+ $c-list-item-disabled-opacity: 0.5 !default;
32
+
33
+ // Focus
34
+ $c-list-focus-outline: 2px solid currentColor !default;
35
+ $c-list-focus-outline-offset: -2px !default;
36
+
37
+ // Motion
38
+ $c-list-transition-duration: 0.15s !default;
@@ -0,0 +1,2 @@
1
+ @forward 'variables';
2
+ @use 'list';