@uncinq/css-components 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Un Cinq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # @uncinq/css-components
2
+
3
+ > Framework-agnostic CSS component implementations — actual styles built on top of design and component tokens.
4
+
5
+ <img width="1280" height="640" alt="share-css-components" src="https://github.com/user-attachments/assets/3d74df8a-e7bf-4ba5-8529-99cfef0bc176" />
6
+
7
+ ## What is this?
8
+
9
+ `@uncinq/css-components` provides the CSS implementations of common UI components. It sits at the top of the CSS stack:
10
+
11
+ ```text
12
+ @uncinq/design-tokens ← primitive + semantic CSS custom properties
13
+ @uncinq/component-tokens ← component-scoped CSS custom properties
14
+ @uncinq/css-base ← base CSS rules
15
+ @uncinq/css-components ← actual CSS rules (this package)
16
+ ```
17
+
18
+ Each component file contains only CSS rules, using the tokens defined in the two packages above. No JavaScript, no markup opinions — just portable, layered CSS.
19
+
20
+ ---
21
+
22
+ ## CSS cascade layers
23
+
24
+ All component styles are declared inside `@layer components`:
25
+
26
+ ```css
27
+ @layer config, base, layouts, vendors, components;
28
+ ```
29
+
30
+ This means:
31
+
32
+ - Components never bleed into base or layout styles
33
+ - Project-level `@layer components` rules always win over these defaults
34
+ - Vendors (e.g. Splide) sit below project components but above base
35
+
36
+ ---
37
+
38
+ ## Prerequisites
39
+
40
+ This package consumes tokens from:
41
+
42
+ - [@uncinq/design-tokens](https://github.com/uncinq/design-tokens) — semantic CSS custom properties
43
+ - [@uncinq/component-tokens](https://github.com/uncinq/component-tokens) — component-scoped CSS custom properties
44
+ - [@uncinq/css-base](https://github.com/uncinq/css-base) — base CSS rules
45
+
46
+ Import them before this package:
47
+
48
+ ```css
49
+ @import '@uncinq/design-tokens';
50
+ @import '@uncinq/component-tokens';
51
+ @import '@uncinq/css-base';
52
+ @import '@uncinq/css-components';
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ npm install @uncinq/css-components
61
+ # or
62
+ yarn add @uncinq/css-components
63
+ ```
64
+
65
+ ### Usage — full import
66
+
67
+ ```css
68
+ @import '@uncinq/design-tokens';
69
+ @import '@uncinq/component-tokens';
70
+ @import '@uncinq/css-base';
71
+ @import '@uncinq/css-components';
72
+ ```
73
+
74
+ ### Usage — per component
75
+
76
+ ```css
77
+ @import '@uncinq/design-tokens';
78
+ @import '@uncinq/component-tokens';
79
+ @import '@uncinq/css-base';
80
+ @import '@uncinq/css-components/css/component/nav.css';
81
+ @import '@uncinq/css-components/css/component/pagination.css';
82
+ ```
83
+
84
+ ### Usage — CDN (no build step)
85
+
86
+ ```html
87
+ <link rel="stylesheet" href="https://unpkg.com/@uncinq/design-tokens/tokens/index.css">
88
+ <link rel="stylesheet" href="https://unpkg.com/@uncinq/component-tokens/tokens/index.css">
89
+ <link rel="stylesheet" href="https://unpkg.com/@uncinq/css-base/css/index.css">
90
+ <link rel="stylesheet" href="https://unpkg.com/@uncinq/css-components/css/index.css">
91
+ ```
92
+
93
+ ---
94
+
95
+ ## File structure
96
+
97
+ ```text
98
+ css/
99
+ index.css ← imports all component files
100
+ component/
101
+ alert.css ← alert / notification banner
102
+ badge.css ← badge / pill / tag
103
+ banner.css ← full-width banner strip
104
+ breadcrumb.css ← breadcrumb navigation
105
+ button.css ← button (all variants)
106
+ card.css ← card container
107
+ dropdown.css ← dropdown menu
108
+ embed.css ← video / iframe embed wrapper
109
+ list.css ← styled list
110
+ map.css ← embedded map
111
+ media.css ← media object (image + text)
112
+ nav.css ← navigation bar
113
+ nav-accessibility.css ← accessibility skip links / focus helpers
114
+ offcanvas.css ← off-canvas panel / drawer
115
+ pagination.css ← pagination control
116
+ ```
117
+
118
+ ---
119
+
120
+ ## References
121
+
122
+ - [@uncinq/design-tokens](https://github.com/uncinq/design-tokens) — primitive + semantic layers
123
+ - [@uncinq/component-tokens](https://github.com/uncinq/component-tokens) — component token layer
124
+ - [@uncinq/css-base](https://github.com/uncinq/css-base) — base CSS rules
125
+ - [MDN: CSS cascade layers](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers)
@@ -0,0 +1,40 @@
1
+ /* components/alert.css */
2
+ @layer components {
3
+ .alert {
4
+ background-color: var(--alert-color-bg);
5
+ border-radius: var(--alert-border-radius);
6
+ color: var(--alert-color-text);
7
+ display: flex;
8
+ flex-direction: column;
9
+ font-size: var(--alert-font-size);
10
+ gap: var(--alert-gap);
11
+ margin: var(--alert-margin);
12
+ padding: var(--alert-padding);
13
+
14
+ > * {
15
+ margin: 0;
16
+ }
17
+
18
+ .alert-heading {
19
+ align-items: center;
20
+ display: flex;
21
+ font-weight: var(--font-weight-semibold);
22
+ gap: var(--alert-gap);
23
+ }
24
+
25
+ &:has(.icon:first-child):not(:has(.alert-heading)) {
26
+ flex-direction: row;
27
+ align-items: flex-start;
28
+ }
29
+ }
30
+
31
+ /* Variants */
32
+ .alert-danger { --alert-color-bg: var(--color-danger-muted); --alert-color-text: var(--color-danger-strong); }
33
+ .alert-dark { --alert-color-bg: var(--color-dark-muted); --alert-color-text: var(--color-dark-strong); }
34
+ .alert-info { --alert-color-bg: var(--color-info-muted); --alert-color-text: var(--color-info-strong); }
35
+ .alert-light { --alert-color-bg: var(--color-light-muted); --alert-color-text: var(--color-light-strong); }
36
+ .alert-primary { --alert-color-bg: var(--color-primary-muted); --alert-color-text: var(--color-primary-strong); }
37
+ .alert-secondary { --alert-color-bg: var(--color-secondary-muted); --alert-color-text: var(--color-secondary-strong); }
38
+ .alert-success { --alert-color-bg: var(--color-success-muted); --alert-color-text: var(--color-success-strong); }
39
+ .alert-warning { --alert-color-bg: var(--color-warning-muted); --alert-color-text: var(--color-warning-strong); }
40
+ }
@@ -0,0 +1,23 @@
1
+ /* components/badge.css */
2
+ @layer components {
3
+ .badge {
4
+ background-color: var(--badge-color-bg);
5
+ border: var(--badge-border-width) var(--badge-border-style) var(--badge-color-border);
6
+ border-radius: var(--badge-border-radius);
7
+ color: var(--badge-color-text);
8
+ display: inline-flex;
9
+ font-size: var(--badge-font-size);
10
+ font-weight: var(--badge-font-weight);
11
+ padding: var(--badge-padding-y) var(--badge-padding-x);
12
+ }
13
+
14
+ /* Variants */
15
+ .badge-danger { --badge-color-bg: var(--color-danger-muted); --badge-color-text: var(--color-danger-strong); }
16
+ .badge-dark { --badge-color-bg: var(--color-dark-muted); --badge-color-text: var(--color-dark-strong); }
17
+ .badge-info { --badge-color-bg: var(--color-info-muted); --badge-color-text: var(--color-info-strong); }
18
+ .badge-light { --badge-color-bg: var(--color-light-muted); --badge-color-text: var(--color-light-strong); }
19
+ .badge-primary { --badge-color-bg: var(--color-primary-muted); --badge-color-text: var(--color-primary-strong); }
20
+ .badge-secondary { --badge-color-bg: var(--color-secondary-muted); --badge-color-text: var(--color-secondary-strong); }
21
+ .badge-success { --badge-color-bg: var(--color-success-muted); --badge-color-text: var(--color-success-strong); }
22
+ .badge-warning { --badge-color-bg: var(--color-warning-muted); --badge-color-text: var(--color-warning-strong); }
23
+ }
@@ -0,0 +1,29 @@
1
+ /* components/banner.css */
2
+ @layer components {
3
+ .banner {
4
+ background-color: var(--alert-color-bg);
5
+ border-radius: var(--alert-border-radius);
6
+ color: var(--alert-color-text);
7
+ display: flex;
8
+ flex-direction: column;
9
+ font-size: var(--alert-font-size);
10
+ gap: var(--alert-gap);
11
+ padding: var(--alert-padding);
12
+ padding-inline: 0;
13
+ text-align: center;
14
+
15
+ p {
16
+ margin: 0 auto;
17
+ }
18
+ }
19
+
20
+ /* Variants */
21
+ .banner-danger { --alert-color-bg: var(--color-danger-muted); --alert-color-text: var(--color-danger-strong); }
22
+ .banner-dark { --alert-color-bg: var(--color-dark-muted); --alert-color-text: var(--color-dark-strong); }
23
+ .banner-info { --alert-color-bg: var(--color-info-muted); --alert-color-text: var(--color-info-strong); }
24
+ .banner-light { --alert-color-bg: var(--color-light-muted); --alert-color-text: var(--color-light-strong); }
25
+ .banner-primary { --alert-color-bg: var(--color-primary-muted); --alert-color-text: var(--color-primary-strong); }
26
+ .banner-secondary { --alert-color-bg: var(--color-secondary-muted); --alert-color-text: var(--color-secondary-strong); }
27
+ .banner-success { --alert-color-bg: var(--color-success-muted); --alert-color-text: var(--color-success-strong); }
28
+ .banner-warning { --alert-color-bg: var(--color-warning-muted); --alert-color-text: var(--color-warning-strong); }
29
+ }
@@ -0,0 +1,43 @@
1
+ /* components/breadcrumb.css */
2
+ @layer components {
3
+ .breadcrumb-wrapper {
4
+ border-bottom: var(--breadcrumb-border-width) solid var(--breadcrumb-color-border);
5
+ border-top: var(--breadcrumb-border-width) solid var(--breadcrumb-color-border);
6
+ padding-block: var(--breadcrumb-padding);
7
+
8
+ ol {
9
+ display: flex;
10
+ flex-wrap: wrap;
11
+ font-size: var(--breadcrumb-font-size);
12
+ gap: var(--breadcrumb-gap);
13
+ list-style: none;
14
+ margin: 0;
15
+ padding: 0;
16
+ }
17
+
18
+ li {
19
+ align-items: center;
20
+ color: var(--breadcrumb-color-text);
21
+ display: flex;
22
+ gap: var(--breadcrumb-gap);
23
+ margin: 0;
24
+
25
+ a {
26
+ color: inherit;
27
+ }
28
+
29
+ &::before {
30
+ color: var(--breadcrumb-color-separator);
31
+ content: var(--breadcrumb-separator);
32
+ }
33
+
34
+ &:first-child::before {
35
+ display: none;
36
+ }
37
+
38
+ &[aria-current="page"] {
39
+ color: var(--breadcrumb-color-text-active);
40
+ }
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,103 @@
1
+ /* components/button.css */
2
+ @layer components {
3
+ .btn {
4
+ align-items: center;
5
+ appearance: none;
6
+ background-color: var(--btn-color-bg);
7
+ border: var(--btn-border-width) var(--btn-border-style) var(--btn-color-border);
8
+ border-radius: var(--btn-border-radius);
9
+ color: var(--btn-color-text);
10
+ cursor: pointer;
11
+ display: inline-flex;
12
+ font-size: var(--btn-font-size);
13
+ font-weight: var(--btn-font-weight);
14
+ gap: var(--btn-gap);
15
+ justify-content: center;
16
+ padding: var(--btn-padding);
17
+ text-decoration-color: var(--btn-color-text-decoration);
18
+ text-decoration-line: var(--btn-text-decoration-line);
19
+ text-decoration-thickness: var(--text-decoration-thickness);
20
+ text-transform: var(--btn-text-transform);
21
+ text-underline-offset: var(--text-decoration-offset);
22
+
23
+ &:focus-visible {
24
+ outline: 2px solid var(--color-focus);
25
+ outline-offset: 2px;
26
+ }
27
+
28
+ &:disabled,
29
+ &[aria-disabled='true'] {
30
+ cursor: not-allowed;
31
+ opacity: 0.5;
32
+ }
33
+
34
+ @media (hover: hover) and (pointer: fine) {
35
+ &:hover {
36
+ background-color: var(--btn-color-bg-hover);
37
+ color: var(--btn-color-text-hover);
38
+ text-decoration-color: var(--btn-color-text-decoration-hover);
39
+ }
40
+ }
41
+
42
+ @media (prefers-reduced-motion: no-preference) {
43
+ transition: var(--btn-transition);
44
+ }
45
+ }
46
+
47
+ /* Variants */
48
+ .btn-danger {
49
+ --btn-color-bg: var(--color-danger);
50
+ --btn-color-border: var(--color-danger);
51
+ --btn-color-text: var(--color-text-on-danger);
52
+ }
53
+
54
+ .btn-ghost {
55
+ --btn-color-bg: transparent;
56
+ --btn-color-border: transparent;
57
+ --btn-color-text: var(--color-text);
58
+ }
59
+
60
+ .btn-primary {
61
+ --btn-color-bg: var(--color-brand);
62
+ --btn-color-border: var(--color-brand);
63
+ --btn-color-text: var(--color-text-on-brand);
64
+ }
65
+
66
+ .btn-secondary {
67
+ --btn-color-bg: var(--color-secondary);
68
+ --btn-color-border: var(--color-secondary);
69
+ --btn-color-text: var(--color-text-on-secondary);
70
+ }
71
+
72
+ .btn-success {
73
+ --btn-color-bg: var(--color-success);
74
+ --btn-color-border: var(--color-success);
75
+ --btn-color-text: var(--color-text-on-success);
76
+ }
77
+
78
+ /* Sizes */
79
+ .btn-xs {
80
+ --btn-font-size: var(--font-size-xs);
81
+ --btn-padding: var(--spacing-xs);
82
+ }
83
+
84
+ .btn-sm {
85
+ --btn-font-size: var(--font-size-sm);
86
+ --btn-padding: var(--spacing-sm);
87
+ }
88
+
89
+ .btn-md {
90
+ --btn-font-size: var(--font-size-md);
91
+ --btn-padding: var(--spacing-md);
92
+ }
93
+
94
+ .btn-lg {
95
+ --btn-font-size: var(--font-size-lg);
96
+ --btn-padding: var(--spacing-lg);
97
+ }
98
+
99
+ .btn-xl {
100
+ --btn-font-size: var(--font-size-xl);
101
+ --btn-padding: var(--spacing-xl);
102
+ }
103
+ }
@@ -0,0 +1,54 @@
1
+ /* components/card.css */
2
+ /*
3
+ * .card is a backwards-compatible alias for .item.
4
+ * Prefer using .item directly: <article class="item page">
5
+ *
6
+ * Structure:
7
+ * <article class="card">
8
+ * <div class="content">
9
+ * <p class="name">...</p>
10
+ * <p class="description">...</p>
11
+ * </div>
12
+ * <div class="media">
13
+ * picture | video
14
+ * </div>
15
+ * </article>
16
+ */
17
+ @layer components {
18
+ .card {
19
+ background-color: var(--card-color-bg);
20
+ border: var(--card-border-width) solid var(--card-color-border);
21
+ border-radius: var(--card-border-radius);
22
+ box-shadow: var(--card-shadow);
23
+ color: var(--card-color-text);
24
+ display: flex;
25
+ flex-direction: column;
26
+ overflow: hidden;
27
+
28
+ .content {
29
+ display: flex;
30
+ flex-direction: column;
31
+ flex: 1;
32
+ gap: var(--card-gap);
33
+ padding: var(--card-padding);
34
+ text-decoration: none;
35
+ }
36
+
37
+ .name {
38
+ color: var(--card-color-title);
39
+ font-size: var(--card-font-size-title);
40
+ font-weight: var(--card-font-weight-title);
41
+ line-height: var(--card-line-height-title);
42
+ }
43
+
44
+ .description {
45
+ color: var(--card-color-text-muted);
46
+ }
47
+
48
+ .media {
49
+ --media-ratio: var(--card-media-ratio);
50
+ --media-color-bg: var(--card-media-color-bg);
51
+ order: -1;
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,125 @@
1
+ /* components/dropdown.css */
2
+ /*
3
+ * .dropdown — contextual menu, vanilla JS driven.
4
+ *
5
+ * JS (components/dropdown.js) manages classes on .dropdown-menu:
6
+ * .showing — open transition starting
7
+ * .show — fully open
8
+ * .hiding — close transition starting
9
+ * aria-expanded on .js-dropdown-toggle
10
+ *
11
+ * Trigger: .js-dropdown-toggle (nextElementSibling must be .dropdown-menu)
12
+ *
13
+ * Expected HTML:
14
+ * <div class="dropdown">
15
+ * <button class="js-dropdown-toggle" aria-expanded="false">Label</button>
16
+ * <ul class="dropdown-menu" role="menu">
17
+ * <li><a class="dropdown-item" href="…">Item</a></li>
18
+ * <li><hr class="dropdown-divider"></li>
19
+ * <li><a class="dropdown-item" href="…">Item</a></li>
20
+ * </ul>
21
+ * </div>
22
+ */
23
+ @layer components {
24
+
25
+ /* ── Container ───────────────────────────────────────────────────── */
26
+
27
+ .dropdown {
28
+ display: inline-flex;
29
+ position: relative;
30
+ }
31
+
32
+ /* ── Trigger ─────────────────────────────────────────────────────── */
33
+
34
+ .dropdown-toggle {
35
+ align-items: center;
36
+ appearance: none;
37
+ border: 0;
38
+ cursor: pointer;
39
+ display: inline-flex;
40
+ padding: var(--spacing-xs);
41
+
42
+ &::before {
43
+ content: '';
44
+ inset: 0;
45
+ position: absolute;
46
+ }
47
+
48
+ /* Chevron via CSS — rotate when open */
49
+ &::after {
50
+ border-left: 0.3em solid transparent;
51
+ border-right: 0.3em solid transparent;
52
+ border-top: 0.3em solid currentColor;
53
+ content: "";
54
+ display: inline-block;
55
+ transition: transform var(--dropdown-transition-duration) var(--dropdown-transition-easing);
56
+ }
57
+
58
+ &[aria-expanded="true"]::after {
59
+ transform: rotate(180deg);
60
+ }
61
+ }
62
+
63
+ /* ── Menu panel ──────────────────────────────────────────────────── */
64
+
65
+ .dropdown-menu {
66
+ background-color: var(--dropdown-color-bg);
67
+ border: var(--dropdown-border-width) var(--dropdown-border-style) var(--dropdown-color-border);
68
+ border-radius: var(--dropdown-border-radius);
69
+ box-shadow: var(--dropdown-shadow);
70
+ color: var(--dropdown-color-text);
71
+ list-style: none;
72
+ min-width: var(--dropdown-min-width);
73
+ opacity: 0;
74
+ padding: var(--dropdown-padding, 0);
75
+ pointer-events: none;
76
+ position: absolute;
77
+ top: 100%;
78
+ transform: translateY(var(--dropdown-translate-y));
79
+ transition-duration: var(--dropdown-transition-duration);
80
+ transition-property: opacity, transform, visibility;
81
+ transition-timing-function: var(--dropdown-transition-easing);
82
+ visibility: hidden;
83
+ z-index: var(--dropdown-z-index);
84
+
85
+ /* Visible during all transition phases */
86
+ &.show,
87
+ &.showing,
88
+ &.hiding {
89
+ pointer-events: auto;
90
+ visibility: visible;
91
+ }
92
+
93
+ /* Fully open */
94
+ &.show {
95
+ opacity: 1;
96
+ transform: translateY(0);
97
+ }
98
+
99
+ /* ── Items ───────────────────────────────────────────────────────── */
100
+
101
+ li a {
102
+ border-radius: var(--dropdown-item-border-radius);
103
+ color: var(--dropdown-color-text);
104
+ font-size: var(--dropdown-item-font-size);
105
+ display: block;
106
+ padding: var(--dropdown-item-padding);
107
+ text-decoration: none;
108
+ white-space: nowrap;
109
+
110
+ @media (hover: hover) and (pointer: fine) {
111
+ &:hover {
112
+ background-color: var(--dropdown-item-color-bg-hover);
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+
119
+ /* ── Alignment variants ──────────────────────────────────────────── */
120
+
121
+ .dropdown-menu-end {
122
+ left: auto;
123
+ right: 0;
124
+ }
125
+ }
@@ -0,0 +1,26 @@
1
+ /* components/embed.css */
2
+ /*
3
+ * .embed — responsive iframe/embed container.
4
+ * Maintains aspect ratio for third-party embeds (video, map, social…).
5
+ *
6
+ * Structure:
7
+ * .embed
8
+ * iframe | video | embed | object
9
+ */
10
+ @layer components {
11
+ .embed {
12
+ aspect-ratio: var(--embed-ratio);
13
+ border: var(--embed-border-width) var(--embed-border-style) var(--embed-color-border);
14
+ border-radius: var(--embed-border-radius);
15
+ box-shadow: var(--embed-box-shadow);
16
+ overflow: hidden;
17
+
18
+ iframe,
19
+ embed,
20
+ object,
21
+ video {
22
+ height: 100%;
23
+ width: 100%;
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,36 @@
1
+ /* components/list.css */
2
+ /*
3
+ * .list — compact labelled list with dash markers.
4
+ *
5
+ * Structure:
6
+ * div.list
7
+ * p (optional label)
8
+ * ul | ol
9
+ * li
10
+ */
11
+ @layer components {
12
+ .list {
13
+ p {
14
+ font-weight: var(--list-label-font-weight);
15
+ margin-bottom: var(--list-label-margin-bottom);
16
+ }
17
+
18
+ &,
19
+ ul,
20
+ ol {
21
+ list-style: none;
22
+ margin-bottom: 0;
23
+ padding-inline-start: 0;
24
+ }
25
+
26
+ li {
27
+ font-size: var(--list-item-font-size);
28
+ margin-block: var(--list-item-gap);
29
+
30
+ &::before {
31
+ content: var(--list-item-marker);
32
+ margin-inline-end: var(--list-item-marker-gap);
33
+ }
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,25 @@
1
+ /* components/map.css */
2
+ /*
3
+ * .map — map container, Leaflet-powered via js-map.
4
+ *
5
+ * Structure:
6
+ * div.map.js-map[data-markers][data-zoom][data-tile][data-marker-hidden]
7
+ */
8
+ @layer components {
9
+ .map {
10
+ aspect-ratio: var(--map-ratio);
11
+ background-color: var(--map-color-bg);
12
+ margin-block: var(--map-margin-block);
13
+ overflow: hidden;
14
+
15
+ @media (--mobile-only) {
16
+ --map-ratio: var(--map-ratio-mobile);
17
+ .container & {
18
+ margin-inline: calc(-1 * var(--gutter));
19
+ }
20
+ .row {
21
+ display: block;
22
+ }
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,40 @@
1
+ /* components/media.css */
2
+ /*
3
+ * .media — structural base for all media components.
4
+ * .media-logo — logo media component.
5
+ * .media-icon — icon media component.
6
+ */
7
+ @layer components {
8
+
9
+ .media {
10
+ aspect-ratio: var(--media-ratio);
11
+ background-color: var(--media-color-bg);
12
+ overflow: hidden;
13
+
14
+ picture,
15
+ img {
16
+ height: 100%;
17
+ object-fit: cover;
18
+ width: 100%;
19
+ }
20
+ }
21
+
22
+ .media-logo {
23
+ picture {
24
+ align-items: center;
25
+ display: flex;
26
+ justify-content: center;
27
+ }
28
+ img {
29
+ max-height: 70%;
30
+ max-width: 60%;
31
+ object-fit: contain;
32
+ }
33
+ }
34
+
35
+ .media-icon {
36
+ --media-ratio: none;
37
+ --media-color-bg: transparent;
38
+ }
39
+
40
+ }
@@ -0,0 +1,39 @@
1
+ /* components/nav-accessibility.css */
2
+ /*
3
+ * .nav-accessibility — skip navigation links for keyboard/AT users.
4
+ *
5
+ * Hidden off-screen by default; slides in when a child receives focus.
6
+ * Must be the first focusable element in <body>.
7
+ *
8
+ * Expected HTML (partials/nav/accessibility.html):
9
+ * <ul class="nav nav-accessibility">
10
+ * <li><a href="#main">…</a></li>
11
+ * <li><a href="#navigation">…</a></li>
12
+ * <li><a href="#footer">…</a></li>
13
+ * </ul>
14
+ */
15
+ @layer components {
16
+ .nav-accessibility {
17
+ flex-direction: row;
18
+ font-size: var(--font-size-xs);
19
+ left: 0;
20
+ overflow: hidden;
21
+ position: absolute;
22
+ transform: translateY(-100%);
23
+ transition: transform var(--duration-fast) var(--easing-out);
24
+ z-index: 300;
25
+
26
+ &:focus-within {
27
+ transform: translateY(0);
28
+ }
29
+
30
+ a {
31
+ background: var(--color-bg);
32
+ &:focus {
33
+ background: var(--color-text);
34
+ color: var(--color-bg);
35
+ outline: none;
36
+ }
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,80 @@
1
+ /* components/nav.css */
2
+ /*
3
+ * .nav — base navigation list, vertical by default.
4
+ *
5
+ * Variants:
6
+ * .nav-title — bold group heading (not a link)
7
+ * .nav-social — icon-only social links
8
+ * .nav-language — compact language switcher
9
+ *
10
+ * Structure:
11
+ * <nav>
12
+ * <ul>
13
+ * <li>
14
+ * <a href="...">...</a>
15
+ * </li>
16
+ * </ul>
17
+ * </nav>
18
+ *
19
+ * Direction is set by context (e.g. header sets flex-direction: row).
20
+ */
21
+ @layer components {
22
+ .nav {
23
+ ul,
24
+ ol {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: var(--nav-gap);
28
+ list-style: none;
29
+ margin: 0;
30
+ padding: 0;
31
+ }
32
+
33
+ /* ── Nav item ────────────────────────────────────────────────── */
34
+
35
+ li {
36
+ position: relative;
37
+ }
38
+
39
+ /* ── Links ───────────────────────────────────────────────────── */
40
+
41
+ a {
42
+ display: block;
43
+ padding: var(--nav-padding-y) var(--nav-padding-x);
44
+ }
45
+
46
+ a:not(.btn) {
47
+ color: var(--nav-color-text);
48
+ &.active {
49
+ --nav-color-text: var(--nav-color-text-active);
50
+ }
51
+ }
52
+
53
+ /* Non-link items (current page rendered as span) */
54
+ li > span {
55
+ color: var(--nav-color-text);
56
+ display: block;
57
+ padding: var(--nav-padding-y) var(--nav-padding-x);
58
+ }
59
+ }
60
+
61
+ /* ── Nav title ───────────────────────────────────────────────────── */
62
+
63
+ .nav-title {
64
+ font-size: var(--nav-font-size-title);
65
+ font-weight: var(--nav-font-weight-title);
66
+ margin: 0;
67
+ padding: var(--nav-padding-y) var(--nav-padding-x);
68
+ }
69
+
70
+ .nav-item-highlighted {
71
+ line-height: 1.2;
72
+ margin-inline: var(--nav-padding-x);
73
+ &:first-child {
74
+ margin-inline-start: 0;
75
+ }
76
+ &:last-child {
77
+ margin-inline-end: 0;
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,177 @@
1
+ /* components/offcanvas.css */
2
+ /*
3
+ * .offcanvas — slide-in panel, vanilla JS driven.
4
+ *
5
+ * JS (components/offcanvas.js) manages:
6
+ * .showing — panel becoming visible (open transition)
7
+ * .show — panel fully open
8
+ * .hiding — panel becoming hidden (close transition)
9
+ * aria-hidden="true" when closed
10
+ * body.offcanvas-open when panel is open
11
+ *
12
+ * Triggers: .js-offcanvas-toggle[data-target="#id"]
13
+ * Close: .js-offcanvas-close (inside panel)
14
+ *
15
+ * Position variants: .offcanvas-start | .offcanvas-end | .offcanvas-top | .offcanvas-bottom
16
+ *
17
+ * Expected HTML:
18
+ * <div id="navigation" class="offcanvas offcanvas-start" role="dialog" aria-modal="true" aria-hidden="true">
19
+ * <div class="header">
20
+ * <span class="title">...</span>
21
+ * <button class="js-offcanvas-close btn">...</button>
22
+ * </div>
23
+ * <div class="content">...</div>
24
+ * </div>
25
+ */
26
+ @layer components {
27
+
28
+ /* ── Panel ───────────────────────────────────────────────────────── */
29
+
30
+ .offcanvas {
31
+ background-color: var(--offcanvas-color-bg);
32
+ bottom: var(--offcanvas-bottom, 0);
33
+ box-shadow: var(--offcanvas-shadow);
34
+ color: var(--offcanvas-color-text);
35
+ display: flex;
36
+ flex-direction: column;
37
+ inset-inline-start: var(--offcanvas-start);
38
+ inset-inline-end: var(--offcanvas-end);
39
+ max-width: 100%;
40
+ overflow: hidden;
41
+ position: fixed;
42
+ top: var(--offcanvas-top, 0);
43
+ transform: var(--offcanvas-translate);
44
+ transition-duration: var(--offcanvas-transition-duration);
45
+ transition-property: transform, visibility;
46
+ transition-timing-function: var(--offcanvas-transition-easing);
47
+ visibility: hidden;
48
+ width: var(--offcanvas-width);
49
+ z-index: var(--offcanvas-z-index);
50
+
51
+ /* Visible during transitions and when open */
52
+ &.show,
53
+ &.showing,
54
+ &.hiding {
55
+ visibility: visible;
56
+ }
57
+
58
+ /* Fully open — remove transform */
59
+ &.show {
60
+ transform: none;
61
+ }
62
+
63
+ /* ── Header ────────────────────────────────────────────────────── */
64
+
65
+ & > .header {
66
+ align-items: center;
67
+ border-bottom: 1px solid var(--offcanvas-color-border);
68
+ display: flex;
69
+ flex-shrink: 0;
70
+ justify-content: space-between;
71
+ padding: var(--offcanvas-padding);
72
+
73
+ & .title {
74
+ font-size: var(--font-size-md);
75
+ font-weight: var(--font-weight-heading);
76
+ margin: 0;
77
+ }
78
+ }
79
+
80
+ /* ── Body ──────────────────────────────────────────────────────── */
81
+
82
+ & > .content {
83
+ flex-grow: 1;
84
+ overflow-y: auto;
85
+ padding: var(--offcanvas-padding);
86
+ }
87
+
88
+ /* ── Footer ──────────────────────────────────────────────────────── */
89
+
90
+ & > .footer {
91
+ flex-shrink: 0;
92
+ padding: var(--offcanvas-padding);
93
+ }
94
+ }
95
+
96
+ /* ── Position variants ───────────────────────────────────────────── */
97
+
98
+ .offcanvas-start {
99
+ --offcanvas-end: auto;
100
+ --offcanvas-start: 0;
101
+ --offcanvas-translate: translateX(-100%);
102
+ }
103
+
104
+ .offcanvas-end {
105
+ --offcanvas-start: auto;
106
+ --offcanvas-end: 0;
107
+ --offcanvas-translate: translateX(100%);
108
+ }
109
+
110
+ .offcanvas-top {
111
+ --offcanvas-end: 0;
112
+ --offcanvas-start: 0;
113
+ --offcanvas-top: 0;
114
+ --offcanvas-translate: translateY(-100%);
115
+ height: var(--offcanvas-height);
116
+ }
117
+
118
+ .offcanvas-bottom {
119
+ --offcanvas-end: 0;
120
+ --offcanvas-start: 0;
121
+ --offcanvas-bottom: 0;
122
+ --offcanvas-translate: translateY(100%);
123
+ height: var(--offcanvas-height);
124
+ }
125
+
126
+ /* ── Responsive variants (static above breakpoint) ──────────────── */
127
+
128
+ .offcanvas-tablet {
129
+ @media (--tablet) {
130
+ box-shadow: none;
131
+ overflow: visible;
132
+ position: static;
133
+ transform: none;
134
+ transition: none;
135
+ visibility: visible;
136
+ width: auto;
137
+ z-index: auto;
138
+ }
139
+ }
140
+
141
+ .offcanvas-laptop {
142
+ @media (--laptop) {
143
+ box-shadow: none;
144
+ overflow: visible;
145
+ position: static;
146
+ transform: none;
147
+ transition: none;
148
+ visibility: visible;
149
+ width: auto;
150
+ z-index: auto;
151
+ }
152
+ }
153
+
154
+ /* ── Backdrop ────────────────────────────────────────────────────── */
155
+
156
+ .offcanvas-backdrop {
157
+ background-color: var(--offcanvas-backdrop-color);
158
+ inset: 0;
159
+ position: fixed;
160
+ z-index: calc(var(--offcanvas-z-index) - 1);
161
+
162
+ &.fade {
163
+ opacity: 0;
164
+ transition: opacity var(--offcanvas-transition-duration) var(--offcanvas-transition-easing);
165
+ }
166
+
167
+ &.show {
168
+ opacity: 1;
169
+ }
170
+ }
171
+
172
+ /* ── Body scroll lock ────────────────────────────────────────────── */
173
+
174
+ body.offcanvas-open {
175
+ overflow: hidden;
176
+ }
177
+ }
@@ -0,0 +1,75 @@
1
+ /* components/pagination.css */
2
+ /*
3
+ * .pagination — page navigation list.
4
+ *
5
+ * Structure:
6
+ * nav[role="navigation"]
7
+ * ul.pagination
8
+ * li[.disabled]
9
+ * a.first | a.previous | a.next | a.last (nav controls)
10
+ * li.item[.item-adjacent][.active]
11
+ * a (page number)
12
+ *
13
+ * Icon glyphs for .first, .last, .previous, .next are defined by the theme
14
+ * via a::before / a::after content.
15
+ */
16
+ @layer components {
17
+ .pagination {
18
+ align-items: center;
19
+ display: flex;
20
+ flex-wrap: wrap;
21
+ gap: var(--pagination-gap);
22
+ list-style: none;
23
+ margin-block: 0;
24
+ padding-inline-start: 0;
25
+
26
+ li {
27
+ margin-block: 0;
28
+ }
29
+
30
+ a {
31
+ align-items: center;
32
+ background-color: var(--pagination-item-color-bg);
33
+ border-radius: var(--pagination-item-border-radius);
34
+ color: var(--pagination-item-color-text);
35
+ display: flex;
36
+ font-size: var(--pagination-item-font-size);
37
+ font-weight: var(--pagination-item-font-weight);
38
+ height: var(--pagination-item-size);
39
+ justify-content: center;
40
+ min-width: var(--pagination-item-size);
41
+ padding-inline: var(--spacing-2xs);
42
+ text-decoration: none;
43
+
44
+ &:hover {
45
+ background-color: var(--pagination-item-color-bg-hover);
46
+ color: var(--pagination-item-color-text-hover);
47
+ }
48
+ }
49
+
50
+ /* Active page */
51
+ .active a {
52
+ --pagination-item-color-bg: var(--pagination-item-color-bg-active);
53
+ --pagination-item-color-text: var(--pagination-item-color-text-active);
54
+ cursor: default;
55
+ pointer-events: none;
56
+ }
57
+
58
+ /* Disabled nav control */
59
+ .disabled a {
60
+ opacity: var(--pagination-disabled-opacity);
61
+ pointer-events: none;
62
+ }
63
+
64
+ /* On small screens: hide first/last controls and non-adjacent page numbers */
65
+ @media (--mobile-only) {
66
+ .first,
67
+ .last,
68
+ li:has(.first),
69
+ li:has(.last),
70
+ .item:not(.item-adjacent):not(.active) {
71
+ display: none;
72
+ }
73
+ }
74
+ }
75
+ }
package/css/index.css ADDED
@@ -0,0 +1,16 @@
1
+ /* css/index.css */
2
+ @import 'component/alert.css';
3
+ @import 'component/badge.css';
4
+ @import 'component/banner.css';
5
+ @import 'component/breadcrumb.css';
6
+ @import 'component/button.css';
7
+ @import 'component/card.css';
8
+ @import 'component/dropdown.css';
9
+ @import 'component/embed.css';
10
+ @import 'component/list.css';
11
+ @import 'component/map.css';
12
+ @import 'component/media.css';
13
+ @import 'component/nav.css';
14
+ @import 'component/nav-accessibility.css';
15
+ @import 'component/offcanvas.css';
16
+ @import 'component/pagination.css';
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@uncinq/css-components",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic CSS component implementations.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/uncinq/css-components.git"
9
+ },
10
+ "homepage": "https://github.com/uncinq/css-components#readme",
11
+ "author": {
12
+ "name": "Un Cinq",
13
+ "url": "https://uncinq.dev/"
14
+ },
15
+ "keywords": [
16
+ "css",
17
+ "components",
18
+ "design-system",
19
+ "cascade-layers"
20
+ ],
21
+ "files": [
22
+ "css/"
23
+ ],
24
+ "exports": {
25
+ ".": "./css/index.css",
26
+ "./css/index.css": "./css/index.css",
27
+ "./css/component/*": "./css/component/*"
28
+ },
29
+ "style": "./css/index.css",
30
+ "peerDependencies": {
31
+ "@uncinq/design-tokens": "*",
32
+ "@uncinq/component-tokens": "*"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }