@ghostly-ui/core 0.2.1 → 0.2.3

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 (2) hide show
  1. package/dist/ghostly.css +137 -92
  2. package/package.json +1 -1
package/dist/ghostly.css CHANGED
@@ -1,8 +1,9 @@
1
1
  /* ============================================================
2
2
  GHOSTLY — Zero-config skeleton loaders
3
3
 
4
- Usage: add data-ghostly="shimmer|pulse|wave" to any container.
5
- All leaf elements inside become skeleton blocks automatically.
4
+ Philosophy: NEVER override layout properties. Only hide content
5
+ and add skeleton background. The component's own CSS defines
6
+ all sizing, spacing, and border-radius.
6
7
  ============================================================ */
7
8
 
8
9
  /* --- Custom properties (override these to customize) --- */
@@ -31,25 +32,7 @@
31
32
  }
32
33
 
33
34
  /* ============================================================
34
- 1. SKELETON TARGET shared base for all skeleton elements
35
- ============================================================ */
36
-
37
- /* All targetable elements inside a ghostly container */
38
- [data-ghostly] :where(
39
- h1, h2, h3, h4, h5, h6,
40
- p, span, a, li, td, th, dt, dd,
41
- label, legend, figcaption, caption, summary,
42
- blockquote, cite, q, em, strong, small, mark, code, pre,
43
- button, input, textarea, select, option,
44
- fieldset, dialog, details, address,
45
- time, abbr, sub, sup, del, ins,
46
- img, svg, video, canvas, picture, iframe
47
- ) {
48
- --_ghostly-target: 1;
49
- }
50
-
51
- /* ============================================================
52
- 2. CONTAINER — Blocks interaction while loading
35
+ 1. CONTAINERBlocks interaction while loading
53
36
  ============================================================ */
54
37
 
55
38
  [data-ghostly] {
@@ -59,7 +42,8 @@
59
42
  }
60
43
 
61
44
  /* ============================================================
62
- 3. TEXT ELEMENTS — Become opaque skeleton blocks
45
+ 2. TEXT ELEMENTS — Hide text, show skeleton background
46
+ Only override visual properties. NEVER touch layout.
63
47
  ============================================================ */
64
48
 
65
49
  [data-ghostly] :where(
@@ -74,81 +58,160 @@
74
58
  color: transparent !important;
75
59
  background-color: var(--ghostly-color) !important;
76
60
  background-image: none !important;
77
- border-radius: var(--ghostly-radius) !important;
78
61
  border-color: transparent !important;
79
62
  box-shadow: none !important;
80
63
  text-decoration: none !important;
64
+ text-shadow: none !important;
81
65
  outline: none !important;
66
+ /* Prevent empty elements from collapsing vertically */
82
67
  min-height: 1em;
83
- min-width: 2rem;
68
+ /* Inline elements need display block/inline-block to respect min-height */
69
+ display: var(--_ghostly-display, revert);
84
70
  }
85
71
 
86
- /* Headings get proportional min-height */
87
- [data-ghostly] :where(h1) { min-height: 1.75em; min-width: 40%; }
88
- [data-ghostly] :where(h2) { min-height: 1.5em; min-width: 50%; }
89
- [data-ghostly] :where(h3) { min-height: 1.3em; min-width: 55%; }
90
- [data-ghostly] :where(h4, h5, h6) { min-height: 1.15em; min-width: 45%; }
91
-
92
- /* Paragraphs simulate multi-line text */
93
- [data-ghostly] :where(p) { min-height: 3em; min-width: 80%; }
72
+ /* Make inline elements (span, a, em, etc.) respect min-height/width */
73
+ [data-ghostly] :where(
74
+ span, a, em, strong, small, mark, code, abbr,
75
+ cite, q, sub, sup, del, ins, time, label
76
+ ) {
77
+ --_ghostly-display: inline-block;
78
+ }
94
79
 
95
- /* Code blocks need more height */
96
- [data-ghostly] :where(pre) { min-height: 5em; min-width: 100%; }
80
+ /* ── Layout preservation ──
81
+ The critical problem: when text is empty, flex children collapse to 0 width.
82
+ Solution: force non-leaf containers to grow, and text elements to fill.
83
+ Uses !important to beat utility-class frameworks (Tailwind, etc). */
84
+
85
+ /* Non-leaf containers (divs wrapping text/images) must expand in flex/grid.
86
+ flex-grow:1 makes them fill available space instead of collapsing. */
87
+ [data-ghostly] div,
88
+ [data-ghostly] section,
89
+ [data-ghostly] article,
90
+ [data-ghostly] main,
91
+ [data-ghostly] aside,
92
+ [data-ghostly] header,
93
+ [data-ghostly] footer,
94
+ [data-ghostly] nav {
95
+ flex-grow: 1 !important;
96
+ min-width: 0 !important;
97
+ }
97
98
 
98
- /* Inputs keep their shape */
99
- [data-ghostly] :where(input, textarea, select) {
100
- min-height: 2.5rem;
101
- min-width: 6rem;
99
+ /* Leaf containers (empty divs used as placeholders like avatars)
100
+ should NOT grow they have their own explicit dimensions. */
101
+ [data-ghostly] div:empty {
102
+ flex-grow: 0 !important;
103
+ flex-shrink: 0 !important;
102
104
  }
103
105
 
104
- /* Buttons keep their shape */
105
- [data-ghostly] :where(button) {
106
- min-height: 2.25rem;
107
- min-width: 5rem;
106
+ /* Text elements: fill full width of their parent */
107
+ [data-ghostly] :where(h1, h2, h3, h4, h5, h6, p, pre,
108
+ blockquote, figcaption, caption, summary, address) {
109
+ width: 100%;
108
110
  }
109
111
 
110
- /* ============================================================
111
- 4. CUSTOM LINE COUNT data-ghostly-lines="N"
112
- ============================================================ */
112
+ /* Inline-turned-block: reasonable minimums */
113
+ [data-ghostly] :where(span, a, em, strong, small, label, time, code) { min-width: 3rem; }
114
+ [data-ghostly] :where(button) { min-width: 5rem; }
115
+ [data-ghostly] :where(input, textarea, select) { min-width: 6rem; }
113
116
 
114
- [data-ghostly] :where([data-ghostly-lines="1"]) { min-height: 1em; }
115
- [data-ghostly] :where([data-ghostly-lines="2"]) { min-height: 2em; }
116
- [data-ghostly] :where([data-ghostly-lines="3"]) { min-height: 3em; }
117
- [data-ghostly] :where([data-ghostly-lines="4"]) { min-height: 4em; }
118
- [data-ghostly] :where([data-ghostly-lines="5"]) { min-height: 5em; }
119
- [data-ghostly] :where([data-ghostly-lines="6"]) { min-height: 6em; }
120
- [data-ghostly] :where([data-ghostly-lines="7"]) { min-height: 7em; }
121
- [data-ghostly] :where([data-ghostly-lines="8"]) { min-height: 8em; }
117
+ /* Headings: proportional min-height */
118
+ [data-ghostly] :where(h1) { min-height: 1.75em; }
119
+ [data-ghostly] :where(h2) { min-height: 1.5em; }
120
+ [data-ghostly] :where(h3) { min-height: 1.3em; }
121
+ [data-ghostly] :where(h4, h5, h6) { min-height: 1.15em; }
122
+
123
+ /* Paragraphs: taller to simulate multi-line text */
124
+ [data-ghostly] :where(p) { min-height: 3em; }
125
+
126
+ /* Code blocks: taller */
127
+ [data-ghostly] :where(pre) { min-height: 5em; }
128
+
129
+ /* Interactive elements: reasonable minimums */
130
+ [data-ghostly] :where(input, textarea, select) { min-height: 2.5rem; }
131
+ [data-ghostly] :where(button) { min-height: 2.25rem; }
122
132
 
123
133
  /* ============================================================
124
- 5. MEDIA ELEMENTS — Become solid skeleton blocks
134
+ 3. MEDIA ELEMENTS — Hide content, show skeleton block
135
+ Preserves ALL existing dimensions and border-radius.
125
136
  ============================================================ */
126
137
 
127
138
  [data-ghostly] :where(img, svg, video, canvas, picture, iframe) {
128
- color: transparent !important;
129
139
  background-color: var(--ghostly-color) !important;
130
- border-radius: var(--ghostly-radius) !important;
131
140
  border-color: transparent !important;
132
141
  box-shadow: none !important;
133
142
  }
134
143
 
135
- /* Hide actual content (images, video frames) */
144
+ /* Hide image content without changing dimensions */
136
145
  [data-ghostly] :where(img) {
137
- object-position: -9999px;
146
+ color: transparent !important;
147
+ object-position: -9999px !important;
138
148
  }
139
149
 
140
150
  [data-ghostly] :where(video, iframe) {
141
151
  opacity: 0;
152
+ }
153
+
154
+ /* SVG: hide strokes/fills but keep size */
155
+ [data-ghostly] :where(svg) {
156
+ color: transparent !important;
157
+ fill: transparent !important;
158
+ stroke: transparent !important;
159
+ }
160
+
161
+ /* Only add min-size for SVG icons that might be empty */
162
+ [data-ghostly] :where(svg:empty) {
163
+ min-height: 1.5rem;
164
+ min-width: 1.5rem;
165
+ }
166
+
167
+ /* ============================================================
168
+ 4. EMPTY CONTAINERS — Style divs that act as placeholders
169
+ When a component renders an empty div instead of an img
170
+ (e.g. avatar placeholder), it should still show skeleton.
171
+ ============================================================ */
172
+
173
+ [data-ghostly] :where(div:empty) {
142
174
  background-color: var(--ghostly-color) !important;
143
175
  }
144
176
 
145
- /* Images without explicit dimensions — use aspect-ratio */
146
- [data-ghostly] :where(img:not([width]):not([style*="width"]), picture:not([width])) {
147
- aspect-ratio: 16/9;
148
- width: 100%;
177
+ /* ============================================================
178
+ 5. BORDER RADIUS — Use computed value, fallback to default
179
+ Never override existing border-radius from the component.
180
+ ============================================================ */
181
+
182
+ /* Apply default radius ONLY to elements that don't have one set.
183
+ We use a very low specificity so any component CSS wins. */
184
+ @layer ghostly-defaults {
185
+ [data-ghostly] h1, [data-ghostly] h2, [data-ghostly] h3,
186
+ [data-ghostly] h4, [data-ghostly] h5, [data-ghostly] h6,
187
+ [data-ghostly] p, [data-ghostly] span, [data-ghostly] a,
188
+ [data-ghostly] li, [data-ghostly] td, [data-ghostly] th,
189
+ [data-ghostly] button, [data-ghostly] input,
190
+ [data-ghostly] textarea, [data-ghostly] select,
191
+ [data-ghostly] code, [data-ghostly] pre,
192
+ [data-ghostly] img, [data-ghostly] svg,
193
+ [data-ghostly] video, [data-ghostly] canvas {
194
+ border-radius: var(--ghostly-radius);
195
+ }
149
196
  }
150
197
 
151
- /* Custom aspect ratio via data attribute */
198
+ /* ============================================================
199
+ 6. CUSTOM LINE COUNT — data-ghostly-lines="N"
200
+ ============================================================ */
201
+
202
+ [data-ghostly] :where([data-ghostly-lines="1"]) { min-height: 1em; }
203
+ [data-ghostly] :where([data-ghostly-lines="2"]) { min-height: 2em; }
204
+ [data-ghostly] :where([data-ghostly-lines="3"]) { min-height: 3em; }
205
+ [data-ghostly] :where([data-ghostly-lines="4"]) { min-height: 4em; }
206
+ [data-ghostly] :where([data-ghostly-lines="5"]) { min-height: 5em; }
207
+ [data-ghostly] :where([data-ghostly-lines="6"]) { min-height: 6em; }
208
+ [data-ghostly] :where([data-ghostly-lines="7"]) { min-height: 7em; }
209
+ [data-ghostly] :where([data-ghostly-lines="8"]) { min-height: 8em; }
210
+
211
+ /* ============================================================
212
+ 7. CUSTOM ASPECT RATIO — data-ghostly-ratio
213
+ ============================================================ */
214
+
152
215
  [data-ghostly] :where([data-ghostly-ratio="1/1"]) { aspect-ratio: 1/1 !important; }
153
216
  [data-ghostly] :where([data-ghostly-ratio="4/3"]) { aspect-ratio: 4/3 !important; }
154
217
  [data-ghostly] :where([data-ghostly-ratio="16/9"]) { aspect-ratio: 16/9 !important; }
@@ -156,35 +219,16 @@
156
219
  [data-ghostly] :where([data-ghostly-ratio="3/4"]) { aspect-ratio: 3/4 !important; }
157
220
  [data-ghostly] :where([data-ghostly-ratio="9/16"]) { aspect-ratio: 9/16 !important; }
158
221
 
159
- /* SVG icons (typically small) */
160
- [data-ghostly] :where(svg) {
161
- min-height: 1.5rem;
162
- min-width: 1.5rem;
163
- }
164
-
165
222
  /* ============================================================
166
- 6. DECORATIVE — Strip visual noise
223
+ 8. DECORATIVE — Strip visual noise
167
224
  ============================================================ */
168
225
 
169
226
  [data-ghostly] :where(hr) {
170
227
  border-color: var(--ghostly-color) !important;
171
228
  }
172
229
 
173
- [data-ghostly] :where(
174
- [class*="badge"],
175
- [class*="chip"],
176
- [class*="tag"],
177
- [class*="avatar"]
178
- ) {
179
- color: transparent !important;
180
- background-color: var(--ghostly-color) !important;
181
- background-image: none !important;
182
- border-color: transparent !important;
183
- box-shadow: none !important;
184
- }
185
-
186
230
  /* ============================================================
187
- 7. EXCLUSIONS — data-ghostly-ignore restores original styles
231
+ 9. EXCLUSIONS — data-ghostly-ignore restores original styles
188
232
  ============================================================ */
189
233
 
190
234
  [data-ghostly] [data-ghostly-ignore],
@@ -198,17 +242,15 @@
198
242
  pointer-events: auto;
199
243
  user-select: auto;
200
244
  min-height: initial;
201
- min-width: initial;
202
245
  animation: none !important;
203
246
  }
204
247
 
205
248
  /* ============================================================
206
- 8. ANIMATIONS — Applied via --_ghostly-target marker
249
+ 10. ANIMATIONS
207
250
  ============================================================ */
208
251
 
209
252
  /* --- Shimmer — gradient sweep left to right --- */
210
253
 
211
- [data-ghostly='shimmer'] :where([style*="--_ghostly-target"]),
212
254
  [data-ghostly='shimmer'] :where(
213
255
  h1, h2, h3, h4, h5, h6,
214
256
  p, span, a, li, td, th, dt, dd,
@@ -217,7 +259,8 @@
217
259
  button, input, textarea, select, option,
218
260
  fieldset, dialog, details, address,
219
261
  time, abbr, sub, sup, del, ins,
220
- img, svg, video, canvas, picture, iframe
262
+ img, svg, video, canvas, picture, iframe,
263
+ div:empty
221
264
  ) {
222
265
  background: linear-gradient(
223
266
  90deg,
@@ -246,7 +289,8 @@
246
289
  button, input, textarea, select, option,
247
290
  fieldset, dialog, details, address,
248
291
  time, abbr, sub, sup, del, ins,
249
- img, svg, video, canvas, picture, iframe
292
+ img, svg, video, canvas, picture, iframe,
293
+ div:empty
250
294
  ) {
251
295
  animation: ghostly-pulse var(--ghostly-speed) ease-in-out infinite !important;
252
296
  }
@@ -266,7 +310,8 @@
266
310
  button, input, textarea, select, option,
267
311
  fieldset, dialog, details, address,
268
312
  time, abbr, sub, sup, del, ins,
269
- img, svg, video, canvas, picture, iframe
313
+ img, svg, video, canvas, picture, iframe,
314
+ div:empty
270
315
  ) {
271
316
  animation: ghostly-wave var(--ghostly-speed) ease-in-out infinite !important;
272
317
  }
@@ -295,7 +340,7 @@
295
340
  [data-ghostly='wave'] > :nth-child(n+13) { animation-delay: 960ms !important; }
296
341
 
297
342
  /* ============================================================
298
- 9. SMOOTH TRANSITION — fade out when loading ends
343
+ 11. SMOOTH TRANSITION — fade out when loading ends
299
344
  ============================================================ */
300
345
 
301
346
  [data-ghostly-smooth] :where(
@@ -316,7 +361,7 @@
316
361
  }
317
362
 
318
363
  /* ============================================================
319
- 10. ACCESSIBILITY
364
+ 12. ACCESSIBILITY
320
365
  ============================================================ */
321
366
 
322
367
  @media (prefers-reduced-motion: reduce) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostly-ui/core",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Zero-config skeleton loaders. Wrap your component, done.",
5
5
  "license": "MIT",
6
6
  "type": "module",