@studiocms/ui 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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +564 -0
  3. package/package.json +49 -0
  4. package/src/components/BaseHead.astro +22 -0
  5. package/src/components/Button.astro +338 -0
  6. package/src/components/Card.astro +62 -0
  7. package/src/components/Center.astro +16 -0
  8. package/src/components/Checkbox.astro +180 -0
  9. package/src/components/Divider.astro +39 -0
  10. package/src/components/Dropdown/Dropdown.astro +253 -0
  11. package/src/components/Dropdown/dropdown.ts +170 -0
  12. package/src/components/Dropdown/index.ts +2 -0
  13. package/src/components/Input.astro +93 -0
  14. package/src/components/Modal/Modal.astro +164 -0
  15. package/src/components/Modal/index.ts +2 -0
  16. package/src/components/Modal/modal.ts +129 -0
  17. package/src/components/RadioGroup.astro +175 -0
  18. package/src/components/Row.astro +38 -0
  19. package/src/components/SearchSelect.astro +430 -0
  20. package/src/components/Select.astro +334 -0
  21. package/src/components/Sidebar/Double.astro +91 -0
  22. package/src/components/Sidebar/Single.astro +42 -0
  23. package/src/components/Sidebar/helpers.ts +133 -0
  24. package/src/components/Sidebar/index.ts +3 -0
  25. package/src/components/Textarea.astro +102 -0
  26. package/src/components/ThemeToggle.astro +40 -0
  27. package/src/components/Toast/Toaster.astro +330 -0
  28. package/src/components/Toast/index.ts +2 -0
  29. package/src/components/Toast/toast.ts +16 -0
  30. package/src/components/Toggle.astro +146 -0
  31. package/src/components/User.astro +68 -0
  32. package/src/components/index.ts +25 -0
  33. package/src/components.ts +24 -0
  34. package/src/css/colors.css +106 -0
  35. package/src/css/global.css +2 -0
  36. package/src/css/resets.css +55 -0
  37. package/src/env.d.ts +15 -0
  38. package/src/icons/Checkmark.astro +13 -0
  39. package/src/icons/ChevronUpDown.astro +13 -0
  40. package/src/icons/User.astro +13 -0
  41. package/src/icons/X-Mark.astro +13 -0
  42. package/src/layouts/RootLayout.astro +34 -0
  43. package/src/layouts/index.ts +2 -0
  44. package/src/layouts.ts +1 -0
  45. package/src/types/index.ts +11 -0
  46. package/src/utils/Icon.astro +41 -0
  47. package/src/utils/ThemeHelper.ts +127 -0
  48. package/src/utils/colors.ts +1 -0
  49. package/src/utils/generateID.ts +5 -0
  50. package/src/utils/headers.ts +190 -0
  51. package/src/utils/iconStrings.ts +29 -0
  52. package/src/utils/iconType.ts +3 -0
  53. package/src/utils/index.ts +1 -0
@@ -0,0 +1,253 @@
1
+ ---
2
+ import Icon from '../../utils/Icon.astro';
3
+ import type { StudioCMSColorway } from '../../utils/colors';
4
+ import type { HeroIconName } from '../../utils/iconType';
5
+
6
+ interface Option {
7
+ label: string;
8
+ value: string;
9
+ disabled?: boolean;
10
+ color?: StudioCMSColorway;
11
+ icon?: HeroIconName;
12
+ href?: string;
13
+ };
14
+
15
+ interface Props {
16
+ options: Option[];
17
+ disabled?: boolean;
18
+ id: string;
19
+ align?: 'start' | 'center' | 'end';
20
+ triggerOn?: 'left' | 'right' | 'both';
21
+ offset?: number;
22
+ };
23
+
24
+ const { options, disabled = false, align = 'center', id, triggerOn = 'left', offset = 0 } = Astro.props;
25
+ ---
26
+ <div
27
+ class="dropdown-container"
28
+ class:list={[disabled && 'disabled']}
29
+ data-align={align}
30
+ id={`${id}-container`}
31
+ data-trigger={triggerOn}
32
+ transition:persist transition:persist-props
33
+ >
34
+ <div class="dropdown-toggle" id={`${id}-toggle-btn`}>
35
+ <slot />
36
+ </div>
37
+ <ul class="dropdown above" class:list={[align]} role="listbox" id={`${id}-dropdown`} transition:persist transition:persist-props>
38
+ {options.map(({ value, disabled, color, label, icon, href }) => (
39
+ <li
40
+ class="dropdown-option"
41
+ role="option"
42
+ data-value={value}
43
+ class:list={[disabled && "disabled", icon && "has-icon", color, href && "has-href"]}
44
+ >
45
+ {icon && (
46
+ <Icon width={24} height={24} name={icon} />
47
+ )}
48
+ {href ? (
49
+ <a href={href} class="dropdown-link">{label}</a>
50
+ ) : (
51
+ <span>{label}</span>
52
+ )}
53
+ </li>
54
+ ))}
55
+ </ul>
56
+ </div>
57
+ <style define:vars={{ offset: `${offset}px` }}>
58
+ .dropdown-toggle {
59
+ width: fit-content;
60
+ }
61
+
62
+ .dropdown-container {
63
+ position: relative;
64
+ display: flex;
65
+ flex-direction: column;
66
+ gap: .25rem;
67
+ }
68
+
69
+ .dropdown {
70
+ position: absolute;
71
+ list-style: none;
72
+ margin: 0;
73
+ padding: 0;
74
+ flex-direction: column;
75
+ border-radius: .5rem;
76
+ background-color: hsl(var(--background-step-2));
77
+ overflow: hidden;
78
+ left: 0;
79
+ z-index: 90;
80
+ min-width: 200px;
81
+ max-width: min-content;
82
+
83
+ height: 0;
84
+ border: none;
85
+ pointer-events: none;
86
+ user-select: none;
87
+
88
+ box-shadow: 0px 4px 8px hsl(var(--shadow), 0.5);
89
+ }
90
+
91
+ @keyframes pop-up {
92
+ 0% {
93
+ scale: 0.9;
94
+ opacity: 0;
95
+ }
96
+
97
+ 100% {
98
+ scale: 1;
99
+ opacity: 1;
100
+ user-select: all;
101
+ }
102
+ }
103
+
104
+ @keyframes pop-down {
105
+ 0% {
106
+ scale: 1;
107
+ height: auto;
108
+ border: initial;
109
+ pointer-events: all;
110
+ top: auto;
111
+ bottom: auto;
112
+ border: 1px solid hsl(var(--border));
113
+ opacity: 1;
114
+ user-select: all;
115
+ }
116
+
117
+ 99.9999% {
118
+ scale: 0.9;
119
+ height: auto;
120
+ border: initial;
121
+ pointer-events: all;
122
+ bottom: initial;
123
+ top: auto;
124
+ bottom: auto;
125
+ border: 1px solid hsl(var(--border));
126
+ opacity: 0;
127
+ }
128
+
129
+ 100% {
130
+ height: 0;
131
+ border: none;
132
+ pointer-events: none;
133
+ user-select: none;
134
+ }
135
+ }
136
+
137
+ .dropdown.initialized {
138
+ animation: pop-down .15s ease forwards;
139
+ }
140
+
141
+ .dropdown.initialized.active {
142
+ display: flex;
143
+ border: 1px solid hsl(var(--border));
144
+ height: auto;
145
+ pointer-events: all;
146
+
147
+ animation: pop-up .15s ease forwards;
148
+ }
149
+
150
+ .dropdown.initialized.below {
151
+ top: calc(100% + .25rem + var(--offset)) !important;
152
+ bottom: auto;
153
+
154
+ transform-origin: top center;
155
+ }
156
+
157
+ .dropdown.below.start {
158
+ transform-origin: top left;
159
+ }
160
+
161
+ .dropdown.below.end {
162
+ transform-origin: top right;
163
+ }
164
+
165
+ .dropdown.above {
166
+ top: auto;
167
+ bottom: calc(100% + .25rem + var(--offset)) !important;
168
+ transform-origin: bottom center;
169
+ }
170
+
171
+ .dropdown.above.start {
172
+ transform-origin: bottom left;
173
+ }
174
+
175
+ .dropdown.above.end {
176
+ transform-origin: bottom right;
177
+ }
178
+
179
+ .dropdown-option {
180
+ padding: .5rem .75rem;
181
+ cursor: pointer;
182
+ font-size: .975em;
183
+ transition: all .15s ease;
184
+ display: flex;
185
+ flex-direction: row;
186
+ gap: .5rem;
187
+ align-items: center;
188
+ width: 100%;
189
+ white-space: normal;
190
+ }
191
+
192
+ .dropdown-option:hover {
193
+ background-color: hsl(var(--background-step-3));
194
+ }
195
+
196
+ .dropdown-option.has-href {
197
+ padding: 0;
198
+ }
199
+
200
+ .dropdown-link {
201
+ padding: .5rem .75rem;
202
+ width: 100%;
203
+ }
204
+
205
+ .dropdown-option.primary {
206
+ color: hsl(var(--primary-base));
207
+ }
208
+
209
+ .dropdown-option.primary:hover {
210
+ background-color: hsl(var(--primary-base));
211
+ color: hsl(var(--text-inverted));
212
+ }
213
+
214
+ .dropdown-option.success {
215
+ color: hsl(var(--success-base));
216
+ }
217
+
218
+ .dropdown-option.success:hover {
219
+ background-color: hsl(var(--success-base));
220
+ color: hsl(var(--text-dark));
221
+ }
222
+
223
+ .dropdown-option.warning {
224
+ color: hsl(var(--warning-base));
225
+ }
226
+
227
+ .dropdown-option.warning:hover {
228
+ background-color: hsl(var(--warning-base));
229
+ color: hsl(var(--text-dark));
230
+ }
231
+
232
+ .dropdown-option.danger {
233
+ color: hsl(var(--danger-base));
234
+ }
235
+
236
+ .dropdown-option.danger:hover {
237
+ background-color: hsl(var(--danger-base));
238
+ color: hsl(var(--text-light));
239
+ }
240
+
241
+ .dropdown-option.disabled {
242
+ pointer-events: none;
243
+ color: hsl(var(--text-muted));
244
+ }
245
+
246
+ .dropdown-option.end {
247
+ justify-content: space-between;
248
+ }
249
+
250
+ .dropdown-option.has-icon {
251
+ padding-left: .5rem;
252
+ }
253
+ </style>
@@ -0,0 +1,170 @@
1
+ class DropdownHelper {
2
+ container: HTMLDivElement;
3
+ toggleEl: HTMLDivElement;
4
+ dropdown: HTMLUListElement;
5
+
6
+ alignment: 'start' | 'center' | 'end';
7
+ triggerOn: 'left' | 'right' | 'both';
8
+ active = false;
9
+ fullWidth = false;
10
+
11
+ constructor(id: string, fullWidth?: boolean) {
12
+ this.container = document.getElementById(`${id}-container`) as HTMLDivElement;
13
+
14
+ if (!this.container) {
15
+ throw new Error(`Unable to find dropdown with ID ${id}.`);
16
+ }
17
+
18
+ this.alignment = this.container.dataset.align as 'start' | 'center' | 'end';
19
+ this.triggerOn = this.container.dataset.trigger as 'left' | 'right' | 'both';
20
+
21
+ this.toggleEl = document.getElementById(`${id}-toggle-btn`) as HTMLDivElement;
22
+ this.dropdown = document.getElementById(`${id}-dropdown`) as HTMLUListElement;
23
+
24
+ if (this.triggerOn === 'left') {
25
+ this.toggleEl.addEventListener('click', this.toggle);
26
+ } else if (this.triggerOn === 'both') {
27
+ this.toggleEl.addEventListener('click', this.toggle);
28
+ this.toggleEl.addEventListener('contextmenu', (e) => {
29
+ e.preventDefault();
30
+ this.toggle();
31
+ });
32
+ } else {
33
+ this.toggleEl.addEventListener('contextmenu', (e) => {
34
+ e.preventDefault();
35
+ this.toggle();
36
+ });
37
+ }
38
+
39
+ if (fullWidth) this.fullWidth = true;
40
+
41
+ window.addEventListener('scroll', this.hide);
42
+ document.addEventListener('astro:before-preparation', () => {
43
+ this.dropdown.classList.remove('initialized');
44
+ });
45
+
46
+ this.hideOnClickOutside(this.container);
47
+
48
+ this.initialOptClickRegistration();
49
+ }
50
+
51
+ public registerClickCallback = (func: (value: string) => void) => {
52
+ const dropdownOpts = this.dropdown.querySelectorAll('li');
53
+
54
+ for (const opt of dropdownOpts) {
55
+ opt.removeEventListener('click', this.hide);
56
+
57
+ opt.addEventListener('click', () => {
58
+ func(opt.dataset.value || '');
59
+ this.hide();
60
+ });
61
+ }
62
+ };
63
+
64
+ private initialOptClickRegistration = () => {
65
+ const dropdownOpts = this.dropdown.querySelectorAll('li');
66
+
67
+ for (const opt of dropdownOpts) {
68
+ opt.addEventListener('click', this.hide);
69
+ }
70
+ };
71
+
72
+ public toggle = () => {
73
+ if (this.active) {
74
+ this.hide();
75
+ return;
76
+ }
77
+
78
+ this.show();
79
+ };
80
+
81
+ public hide = () => {
82
+ this.dropdown.classList.remove('active');
83
+ this.active = false;
84
+
85
+ setTimeout(() => this.dropdown.classList.remove('above', 'below'), 200);
86
+ };
87
+
88
+ public show = () => {
89
+ const isMobile = window.matchMedia('screen and (max-width: 840px)').matches;
90
+
91
+ const {
92
+ bottom,
93
+ left,
94
+ right,
95
+ width: parentWidth,
96
+ x,
97
+ y,
98
+ height,
99
+ } = this.toggleEl.getBoundingClientRect();
100
+ const { width: dropdownWidth } = this.dropdown.getBoundingClientRect();
101
+
102
+ const optionHeight = 43.28;
103
+ const totalBorderSize = 2;
104
+ const margin = 4;
105
+
106
+ const dropdownHeight = this.dropdown.children.length * optionHeight + totalBorderSize + margin;
107
+
108
+ const CustomRect = {
109
+ top: bottom + margin,
110
+ left,
111
+ right,
112
+ bottom: bottom + margin + dropdownHeight,
113
+ width: isMobile || this.fullWidth ? parentWidth : dropdownWidth, // Account for scaling of animation
114
+ height: dropdownHeight,
115
+ x,
116
+ y: y + height + margin,
117
+ };
118
+
119
+ this.active = true;
120
+
121
+ if (isMobile || this.fullWidth) {
122
+ this.dropdown.style.maxWidth = `${parentWidth}px`;
123
+ this.dropdown.style.minWidth = 'unset';
124
+ this.dropdown.style.width = `${parentWidth}px`;
125
+ this.dropdown.style.left = `calc(${parentWidth / 2}px - ${CustomRect.width / 2}px)`;
126
+ } else {
127
+ if (this.alignment === 'end') {
128
+ this.dropdown.style.left = `calc(${parentWidth}px - ${CustomRect.width}px)`;
129
+ }
130
+
131
+ if (this.alignment === 'center') {
132
+ this.dropdown.style.left = `calc(${parentWidth / 2}px - ${CustomRect.width / 2}px)`;
133
+ }
134
+ }
135
+
136
+ if (!this.dropdown.classList.contains('initialized')) {
137
+ this.dropdown.classList.add('initialized');
138
+ }
139
+
140
+ if (
141
+ CustomRect.top >= 0 &&
142
+ CustomRect.left >= 0 &&
143
+ CustomRect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
144
+ CustomRect.right <= (window.innerWidth || document.documentElement.clientWidth)
145
+ ) {
146
+ this.dropdown.classList.add('active', 'below');
147
+ } else {
148
+ this.dropdown.classList.add('active', 'above');
149
+ }
150
+ };
151
+
152
+ private hideOnClickOutside = (element: HTMLElement) => {
153
+ const outsideClickListener = (event: MouseEvent) => {
154
+ if (!event.target) return;
155
+
156
+ if (!element.contains(event.target as Node) && isVisible(element) && this.active === true) {
157
+ // or use: event.target.closest(selector) === null
158
+ this.hide();
159
+ }
160
+ };
161
+
162
+ document.addEventListener('click', outsideClickListener);
163
+ };
164
+ }
165
+
166
+ export { DropdownHelper };
167
+
168
+ // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
169
+ const isVisible = (elem: HTMLElement) =>
170
+ !!elem && !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
@@ -0,0 +1,2 @@
1
+ export { default as Dropdown } from './Dropdown.astro';
2
+ export { DropdownHelper } from './dropdown';
@@ -0,0 +1,93 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types';
3
+ import { generateID } from '../utils/generateID';
4
+
5
+ interface Props extends HTMLAttributes<'input'> {
6
+ label?: string;
7
+ type?: 'text' | 'password' | 'email' | 'number' | 'tel' | 'url';
8
+ placeholder?: string;
9
+ isRequired?: boolean;
10
+ name?: string;
11
+ disabled?: boolean;
12
+ defaultValue?: string;
13
+ class?: string;
14
+ };
15
+
16
+ const {
17
+ label,
18
+ placeholder,
19
+ name = generateID('input'),
20
+ type = 'text',
21
+ defaultValue,
22
+ isRequired = false,
23
+ disabled = false,
24
+ class: className,
25
+ ...props
26
+ } = Astro.props;
27
+ ---
28
+
29
+ <label for={name} class="input-label" class:list={[disabled && "disabled"]}>
30
+ <span class="label">
31
+ {label} <span class="req-star">{isRequired && "*"}</span>
32
+ </span>
33
+ <input
34
+ placeholder={placeholder}
35
+ name={name}
36
+ id={name}
37
+ type={type}
38
+ class="input"
39
+ class:list={[className]}
40
+ required={isRequired}
41
+ disabled={disabled}
42
+ value={defaultValue}
43
+ {...props}
44
+ />
45
+ </label>
46
+ <style>
47
+ .input-label {
48
+ width: 100%;
49
+ display: flex;
50
+ flex-direction: column;
51
+ gap: .25rem;
52
+ margin-top: .5rem;
53
+ }
54
+
55
+ .input-label.disabled {
56
+ opacity: 0.5;
57
+ pointer-events: none;
58
+ color: hsl(var(--text-muted));
59
+ }
60
+
61
+ .label {
62
+ font-size: 14px;
63
+ }
64
+
65
+ .input {
66
+ padding: .5rem 1rem;
67
+ border-radius: 8px;
68
+ border: 1px solid hsl(var(--border));
69
+ background: hsl(var(--background-step-2));
70
+ color: hsl(var(--text-normal));
71
+ transition: all .15s ease;
72
+ }
73
+
74
+ .input:hover {
75
+ background: hsl(var(--background-step-3));
76
+ }
77
+
78
+ .input:active,
79
+ .input:focus {
80
+ border: 1px solid hsl(var(--primary-base));
81
+ outline: none;
82
+ background: hsl(var(--background-step-2));
83
+ }
84
+
85
+ .disabled .input:active {
86
+ border: 1px solid hsl(var(--border));
87
+ }
88
+
89
+ .req-star {
90
+ color: hsl(var(--danger-base));
91
+ font-weight: 700;
92
+ }
93
+ </style>
@@ -0,0 +1,164 @@
1
+ ---
2
+ import Icon from '../../utils/Icon.astro';
3
+ import Button from '../Button.astro';
4
+
5
+ type ModalButton = 'cancel' | 'confirm';
6
+
7
+ interface Props {
8
+ id: string;
9
+ size?: 'sm' | 'md' | 'lg';
10
+ dismissable?: boolean;
11
+ buttons?: ModalButton[];
12
+ isForm?: boolean;
13
+ };
14
+
15
+ const { id, size = 'md', dismissable = true, isForm = false, buttons = [] } = Astro.props;
16
+ ---
17
+ <!-- Should: Be centered on mobile, scroll happens inside, blur bg -->
18
+ <dialog
19
+ popover
20
+ id={id}
21
+ data-dismissable={`${dismissable}`}
22
+ data-buttons={buttons.join(";")}
23
+ class="modal"
24
+ class:list={[size]}
25
+ data-form={`${isForm}`}
26
+ >
27
+ <div class="modal-header">
28
+ <slot name="header" />
29
+ {(dismissable || buttons.length === 0) && (
30
+ <button class="x-mark-container" id={`${id}-btn-x`}>
31
+ <Icon name="x-mark" width={24} height={24} class={'dismiss-icon'} />
32
+ </button>
33
+ )}
34
+ </div>
35
+ <form id={`${id}-form-element`}>
36
+ <div class="modal-body">
37
+ <slot />
38
+ </div>
39
+ <div class="modal-footer">
40
+ {buttons.includes('cancel') && (
41
+ <Button id={`${id}-btn-cancel`} color='danger' variant='flat' type={isForm ? 'reset' : 'button'}>
42
+ Cancel
43
+ </Button>
44
+ )}
45
+ {buttons.includes('confirm') && (
46
+ <Button id={`${id}-btn-confirm`} type={'submit'} color='primary' variant='solid' type={isForm ? 'submit' : 'button'}>
47
+ Confirm
48
+ </Button>
49
+ )}
50
+ </div>
51
+ </form>
52
+ </dialog>
53
+ <style>
54
+ .modal {
55
+ border: 1px solid hsl(var(--border));
56
+ border-radius: .5rem;
57
+ padding: 1.5rem;
58
+ box-shadow: 0px 6px 8px hsl(var(--shadow));
59
+ animation: hide .25s ease;
60
+ overflow: visible;
61
+ margin: auto;
62
+ }
63
+
64
+ .modal.sm {
65
+ width: 384px;
66
+ }
67
+
68
+ .modal.md {
69
+ width: 448px;
70
+ }
71
+
72
+ .modal.lg {
73
+ width: 608px;
74
+ }
75
+
76
+ .modal[open] {
77
+ animation: show .25s ease-in-out;
78
+ }
79
+
80
+ html:has(.modal[open]),
81
+ body:has(.modal[open]) {
82
+ overflow: hidden;
83
+ }
84
+
85
+ .modal[open]::backdrop {
86
+ background-color: rgba(0, 0, 0, 0.75);
87
+ animation: backdrop .3s ease-in-out forwards;
88
+ }
89
+
90
+ .modal-header:has(*) {
91
+ margin-bottom: 1rem;
92
+
93
+ display: flex;
94
+ flex-direction: row;
95
+ justify-content: space-between;
96
+ gap: 1rem;
97
+
98
+ * {
99
+ margin: 0;
100
+ }
101
+ }
102
+
103
+ .x-mark-container {
104
+ cursor: pointer;
105
+ height: 1.5rem;
106
+ width: 1.5rem;
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ transition: background-color .15s ease;
111
+ border-radius: .25rem;
112
+ }
113
+
114
+ .x-mark-container:hover {
115
+ background-color: hsl(var(--default-base));
116
+ }
117
+
118
+ .modal-footer {
119
+ display: none;
120
+ }
121
+
122
+ .modal-footer:has(*) {
123
+ display: flex;
124
+ flex-direction: row;
125
+ gap: 1rem;
126
+ margin-top: 1rem;
127
+ justify-content: end;
128
+ }
129
+
130
+ @keyframes hide {
131
+ 0% {
132
+ scale: 1;
133
+ opacity: 1;
134
+ display: block;
135
+ }
136
+ 100% {
137
+ scale: 0.85;
138
+ opacity: 0;
139
+ display: none;
140
+ }
141
+ }
142
+
143
+ @keyframes show {
144
+ 0% {
145
+ scale: 0.85;
146
+ opacity: 0;
147
+ display: none;
148
+ }
149
+ 100% {
150
+ scale: 1;
151
+ opacity: 1;
152
+ display: block;
153
+ }
154
+ }
155
+
156
+ @keyframes backdrop {
157
+ 0% {
158
+ opacity: 0;
159
+ }
160
+ 100% {
161
+ opacity: 1;
162
+ }
163
+ }
164
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as Modal } from './Modal.astro';
2
+ export { ModalHelper } from './modal';