@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 +74 -2
- package/dist/index.js +68 -0
- package/package.json +54 -4
- package/scss/_list.scss +115 -0
- package/scss/_variables.scss +38 -0
- package/scss/index.scss +2 -0
package/README.md
CHANGED
|
@@ -1,3 +1,75 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @carissa-io/components-list
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|
package/scss/_list.scss
ADDED
|
@@ -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;
|
package/scss/index.scss
ADDED