vuetiwatch 0.1.0 → 0.2.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.
Files changed (164) hide show
  1. package/README.md +200 -17
  2. package/dist/index.d.ts +4 -4
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/plugin.d.ts +77 -7
  7. package/dist/plugin.d.ts.map +1 -1
  8. package/dist/plugin.js +104 -15
  9. package/dist/plugin.js.map +1 -1
  10. package/dist/registry.d.ts +1 -1
  11. package/dist/registry.d.ts.map +1 -1
  12. package/dist/registry.js +6 -1
  13. package/dist/registry.js.map +1 -1
  14. package/dist/styles/atlas.css +99 -0
  15. package/dist/styles/atlas.min.css +1 -0
  16. package/dist/styles/atlasDark.css +99 -0
  17. package/dist/styles/atlasDark.min.css +1 -0
  18. package/dist/styles/atlasSepia.css +99 -0
  19. package/dist/styles/atlasSepia.min.css +1 -0
  20. package/dist/styles/aurora.css +97 -4
  21. package/dist/styles/aurora.min.css +1 -1
  22. package/dist/styles/brutalist.css +5 -0
  23. package/dist/styles/brutalist.min.css +1 -1
  24. package/dist/styles/calm.css +0 -4
  25. package/dist/styles/calm.min.css +1 -1
  26. package/dist/styles/candy.css +71 -0
  27. package/dist/styles/candy.min.css +1 -0
  28. package/dist/styles/clay.css +13 -0
  29. package/dist/styles/clay.min.css +1 -1
  30. package/dist/styles/core.css +239 -36
  31. package/dist/styles/core.min.css +1 -1
  32. package/dist/styles/graphite.css +29 -0
  33. package/dist/styles/graphite.min.css +1 -0
  34. package/dist/styles/morph.css +12 -0
  35. package/dist/styles/morph.min.css +1 -1
  36. package/dist/styles/neon.css +8 -0
  37. package/dist/styles/neon.min.css +1 -1
  38. package/dist/styles/sketchy.css +4 -0
  39. package/dist/styles/sketchy.min.css +1 -1
  40. package/dist/styles/soft.css +1 -12
  41. package/dist/styles/soft.min.css +1 -1
  42. package/dist/styles.css +577 -52
  43. package/dist/styles.min.css +1 -1
  44. package/dist/themes/atlas.d.ts +38 -0
  45. package/dist/themes/atlas.d.ts.map +1 -0
  46. package/dist/themes/atlas.js +203 -0
  47. package/dist/themes/atlas.js.map +1 -0
  48. package/dist/themes/atlasDark.d.ts +8 -0
  49. package/dist/themes/atlasDark.d.ts.map +1 -0
  50. package/dist/themes/atlasDark.js +64 -0
  51. package/dist/themes/atlasDark.js.map +1 -0
  52. package/dist/themes/atlasSepia.d.ts +11 -0
  53. package/dist/themes/atlasSepia.d.ts.map +1 -0
  54. package/dist/themes/atlasSepia.js +60 -0
  55. package/dist/themes/atlasSepia.js.map +1 -0
  56. package/dist/themes/aurora.d.ts.map +1 -1
  57. package/dist/themes/aurora.js +27 -2
  58. package/dist/themes/aurora.js.map +1 -1
  59. package/dist/themes/brutalist.d.ts.map +1 -1
  60. package/dist/themes/brutalist.js +26 -0
  61. package/dist/themes/brutalist.js.map +1 -1
  62. package/dist/themes/calm.d.ts.map +1 -1
  63. package/dist/themes/calm.js +22 -2
  64. package/dist/themes/calm.js.map +1 -1
  65. package/dist/themes/candy.d.ts +20 -0
  66. package/dist/themes/candy.d.ts.map +1 -0
  67. package/dist/themes/candy.js +142 -0
  68. package/dist/themes/candy.js.map +1 -0
  69. package/dist/themes/classic.d.ts.map +1 -1
  70. package/dist/themes/classic.js +2 -0
  71. package/dist/themes/classic.js.map +1 -1
  72. package/dist/themes/clay.d.ts.map +1 -1
  73. package/dist/themes/clay.js +22 -0
  74. package/dist/themes/clay.js.map +1 -1
  75. package/dist/themes/darkGlass.d.ts.map +1 -1
  76. package/dist/themes/darkGlass.js +21 -0
  77. package/dist/themes/darkGlass.js.map +1 -1
  78. package/dist/themes/graphite.d.ts +13 -0
  79. package/dist/themes/graphite.d.ts.map +1 -0
  80. package/dist/themes/graphite.js +147 -0
  81. package/dist/themes/graphite.js.map +1 -0
  82. package/dist/themes/index.d.ts +5 -0
  83. package/dist/themes/index.d.ts.map +1 -1
  84. package/dist/themes/index.js +5 -0
  85. package/dist/themes/index.js.map +1 -1
  86. package/dist/themes/liquidGlass.d.ts.map +1 -1
  87. package/dist/themes/liquidGlass.js +27 -3
  88. package/dist/themes/liquidGlass.js.map +1 -1
  89. package/dist/themes/lux.d.ts.map +1 -1
  90. package/dist/themes/lux.js +21 -2
  91. package/dist/themes/lux.js.map +1 -1
  92. package/dist/themes/morph.d.ts.map +1 -1
  93. package/dist/themes/morph.js +23 -3
  94. package/dist/themes/morph.js.map +1 -1
  95. package/dist/themes/neon.d.ts.map +1 -1
  96. package/dist/themes/neon.js +22 -0
  97. package/dist/themes/neon.js.map +1 -1
  98. package/dist/themes/paper.d.ts.map +1 -1
  99. package/dist/themes/paper.js +16 -0
  100. package/dist/themes/paper.js.map +1 -1
  101. package/dist/themes/sketchy.d.ts.map +1 -1
  102. package/dist/themes/sketchy.js +17 -0
  103. package/dist/themes/sketchy.js.map +1 -1
  104. package/dist/themes/slate.d.ts.map +1 -1
  105. package/dist/themes/slate.js +22 -1
  106. package/dist/themes/slate.js.map +1 -1
  107. package/dist/themes/soft.d.ts.map +1 -1
  108. package/dist/themes/soft.js +21 -2
  109. package/dist/themes/soft.js.map +1 -1
  110. package/dist/types.d.ts +40 -0
  111. package/dist/types.d.ts.map +1 -1
  112. package/dist/util/defaults.d.ts.map +1 -1
  113. package/dist/util/defaults.js +11 -1
  114. package/dist/util/defaults.js.map +1 -1
  115. package/dist/util/defineTheme.d.ts.map +1 -1
  116. package/dist/util/defineTheme.js +11 -5
  117. package/dist/util/defineTheme.js.map +1 -1
  118. package/package.json +2 -2
  119. package/src/index.ts +8 -2
  120. package/src/plugin.ts +214 -21
  121. package/src/registry.ts +10 -0
  122. package/src/styles/core.css +563 -35
  123. package/src/styles/index.css +4 -0
  124. package/src/styles/themes/_atlas.css +194 -0
  125. package/src/styles/themes/atlas.css +2 -0
  126. package/src/styles/themes/atlasDark.css +2 -0
  127. package/src/styles/themes/atlasSepia.css +2 -0
  128. package/src/styles/themes/aurora.css +151 -7
  129. package/src/styles/themes/brutalist.css +5 -3
  130. package/src/styles/themes/calm.css +0 -7
  131. package/src/styles/themes/candy.css +113 -0
  132. package/src/styles/themes/clay.css +24 -0
  133. package/src/styles/themes/graphite.css +63 -0
  134. package/src/styles/themes/liquidGlass.css +0 -2
  135. package/src/styles/themes/lux.css +0 -4
  136. package/src/styles/themes/morph.css +22 -0
  137. package/src/styles/themes/neon.css +8 -3
  138. package/src/styles/themes/paper.css +0 -2
  139. package/src/styles/themes/sketchy.css +9 -0
  140. package/src/styles/themes/slate.css +0 -4
  141. package/src/styles/themes/soft.css +1 -12
  142. package/src/themes/atlas.ts +211 -0
  143. package/src/themes/atlasDark.ts +64 -0
  144. package/src/themes/atlasSepia.ts +60 -0
  145. package/src/themes/aurora.ts +30 -2
  146. package/src/themes/brutalist.ts +29 -0
  147. package/src/themes/calm.ts +25 -2
  148. package/src/themes/candy.ts +144 -0
  149. package/src/themes/classic.ts +3 -0
  150. package/src/themes/clay.ts +24 -0
  151. package/src/themes/darkGlass.ts +24 -0
  152. package/src/themes/graphite.ts +150 -0
  153. package/src/themes/index.ts +5 -0
  154. package/src/themes/liquidGlass.ts +30 -3
  155. package/src/themes/lux.ts +24 -2
  156. package/src/themes/morph.ts +25 -3
  157. package/src/themes/neon.ts +25 -0
  158. package/src/themes/paper.ts +19 -0
  159. package/src/themes/sketchy.ts +20 -0
  160. package/src/themes/slate.ts +25 -1
  161. package/src/themes/soft.ts +24 -2
  162. package/src/types.ts +41 -0
  163. package/src/util/defaults.ts +12 -1
  164. package/src/util/defineTheme.ts +11 -5
@@ -10,19 +10,31 @@
10
10
  * The scope is either a Vuetiwatch theme class or the `data-vuetiwatch`
11
11
  * attribute `createVuetiwatch()` sets on `<html>` — the latter is what
12
12
  * extends this layer to themes you build yourself with `defineTheme()`.
13
- * `VUETIWATCH_THEMES` is a placeholder: the build expands it into one class
14
- * per registered theme, so this list can never drift from the registry.
13
+ * `VUETIWATCH_THEMES` and `VUETIWATCH_STOCK` are placeholders: the build
14
+ * expands the first into one class per registered theme and the second into
15
+ * the attribute value of every theme that declared `meta.stock`, so neither
16
+ * list can drift from the registry. A stock theme is excluded twice over —
17
+ * by class and by attribute — which is what makes `classic` a control
18
+ * rather than another theme.
15
19
  * Importing this file therefore cannot affect an app that is not running a
16
20
  * Vuetiwatch theme, and every value falls back to the stock Vuetify one, so
17
21
  * a theme only pays for what it overrides.
18
22
  *
19
- * Component classes are doubled to out-specify Vuetify's own rules without
20
- * relying on an ancestor, which keeps teleported overlays covered.
23
+ * Weight is deliberate, and it runs one way: every rule here has to beat
24
+ * Vuetify and lose to everything else. It beats Vuetify for free — all but
25
+ * six of its rules ship inside `@layer vuetify-*`, and an unlayered author
26
+ * rule outranks a layered one whatever its specificity — so the scope below
27
+ * sits in `:where()` and costs nothing. What is left is the component
28
+ * selector alone, which a theme file (a theme class plus the same selector)
29
+ * and an app's own stylesheet both clear without a fight. Vuetify's utility
30
+ * classes are the exception in the other direction: they carry `!important`,
31
+ * which no specificity reaches, so the rules here step around them with
32
+ * `:not([class*='bg-'])` and its siblings instead.
21
33
  *
22
34
  * Authored with CSS nesting; the published file is flattened at build time.
23
35
  */
24
36
 
25
- :is([data-vuetiwatch], .v-theme--VUETIWATCH_THEMES) {
37
+ :where([data-vuetiwatch]:not(VUETIWATCH_STOCK), .v-theme--VUETIWATCH_THEMES) {
26
38
  /* -------------------------------------------------------------- radius */
27
39
 
28
40
  /**
@@ -35,7 +47,11 @@
35
47
  * those children individually turns a joined group into separate pills
36
48
  * and a full-height toolbar button into a rounded slab.
37
49
  */
38
- & .v-btn.v-btn:not(
50
+ & .v-btn--icon:not([class*='rounded-'], .v-btn-group *, .v-pagination *) {
51
+ border-radius: var(--v-vw-radius-icon-btn, 50%);
52
+ }
53
+
54
+ & .v-btn:not(
39
55
  .v-btn--icon,
40
56
  .v-btn--rounded,
41
57
  [class*='rounded-'],
@@ -46,6 +62,104 @@
46
62
  border-radius: var(--v-vw-radius-btn, var(--v-vw-radius, 4px));
47
63
  }
48
64
 
65
+ /**
66
+ * A list standing on its own is a panel and takes the panel corner. One
67
+ * inside a card, a drawer, a menu or another list is not: that surface
68
+ * owns the corner already, and rounding the inner box would clip its own
69
+ * rows for nothing. Vuetify ships `.v-list` at radius 0 with no edge at
70
+ * all, which is why a bare list was the one surface that stayed stock
71
+ * whatever theme was running.
72
+ */
73
+ & :is(.v-list, .v-sheet, .v-banner, .v-table, .v-skeleton-loader):not(:is(.v-card, .v-overlay__content, .v-navigation-drawer, .v-expansion-panels, .v-list) *) {
74
+ border-radius: var(--v-vw-radius-lg, var(--v-vw-radius, 4px));
75
+ }
76
+
77
+ /**
78
+ * And what that panel is drawn with. Fifteen themes had written this
79
+ * selector out in full — the same four components, the same five
80
+ * containers that own their children's edges — and each carried its own
81
+ * copy of a list that has had to grow twice. The selector belongs in one
82
+ * place; the treatment is a value, so it travels as one.
83
+ *
84
+ * Every fallback is `revert-layer`, which hands the property back to
85
+ * Vuetify's own layer, so a theme that says nothing here changes nothing.
86
+ * The radius is above rather than in this block because it comes from the
87
+ * shape scale every other surface uses, and a skeleton loader takes the
88
+ * corner without taking the edge.
89
+ */
90
+ & :is(.v-list, .v-sheet, .v-banner, .v-table):not(:is(.v-card, .v-overlay__content, .v-navigation-drawer, .v-expansion-panels, .v-list) *) {
91
+ border: var(--v-vw-panel-border, revert-layer);
92
+ box-shadow: var(--v-vw-panel-shadow, revert-layer);
93
+ background-color: var(--v-vw-panel-fill, revert-layer);
94
+ background-image: var(--v-vw-panel-image, revert-layer);
95
+ backdrop-filter: var(--v-vw-panel-filter, none);
96
+ }
97
+
98
+ /**
99
+ * `border` on a list or a sheet is the author asking for an edge, and the
100
+ * shorthand above would answer with Vuetify's hairline. The width stays
101
+ * the theme's — which is how a theme that draws no borders at all keeps
102
+ * its zero, and how `brutalist` keeps its three pixels.
103
+ */
104
+ & :is(.v-list--border, .v-sheet--border):not(:is(.v-card, .v-overlay__content, .v-navigation-drawer, .v-expansion-panels, .v-list) *) {
105
+ border-width: var(--v-vw-border-width, thin);
106
+ }
107
+
108
+ /**
109
+ * `.v-sheet` is Vuetify's surface primitive, so this one selector reaches
110
+ * the stepper and the pickers as well — components with enough layout of
111
+ * their own that no theme had thought to name them.
112
+ */
113
+
114
+ /**
115
+ * A button group is one control, so the corner belongs to the group and
116
+ * not to the buttons inside it — which is why the rule above steps over
117
+ * `.v-btn-group *`. Without this the group kept Vuetify's 8px while every
118
+ * button in the theme was square.
119
+ */
120
+ & :is(.v-btn-group, .v-btn-toggle):not([class*='rounded-']) {
121
+ border-radius: var(--v-vw-radius-btn, var(--v-vw-radius, 4px));
122
+ }
123
+
124
+ /**
125
+ * Vuetify's outlined card is transparent, so it shows the page through
126
+ * itself and so does everything nested in it — a list, a table, a tree.
127
+ * On the stock theme that is invisible, because its background and its
128
+ * surface are both white; on a theme with a tinted ground it means half
129
+ * the panels on a screen are panels and half are holes.
130
+ *
131
+ * Material 3 gives an outlined card the surface colour, and so does this:
132
+ * a card is a container, and a container that is not there cannot hold
133
+ * anything. The relief and glass themes, whose whole idea is that a
134
+ * surface is the ground, set the knob back to `transparent`.
135
+ */
136
+ & .v-card--variant-outlined:not([class*='bg-']) {
137
+ background-color: var(--v-vw-outlined-fill, rgb(var(--v-theme-surface)));
138
+ }
139
+
140
+ /* A badge is a very small chip and rounds like one. */
141
+ & .v-badge__badge {
142
+ border-radius: var(--v-vw-radius-chip, 9999px);
143
+ }
144
+
145
+ /**
146
+ * Avatars and timeline dots are circles in Material and stay circles by
147
+ * default. They get their own knobs because a theme with no curve
148
+ * anywhere — `brutalist`, `neon` — was still drawing two of them.
149
+ */
150
+ & .v-avatar:not([class*='rounded-']) {
151
+ border-radius: var(--v-vw-radius-avatar, 50%);
152
+ }
153
+
154
+ & :is(.v-timeline-divider__dot, .v-timeline-divider__inner-dot) {
155
+ border-radius: var(--v-vw-radius-dot, 50%);
156
+ }
157
+
158
+ /* The `rounded` prop is the pill; everything else follows the theme. */
159
+ & .v-progress-linear:not(.v-progress-linear--rounded, [class*='rounded-']) {
160
+ border-radius: var(--v-vw-radius, 0);
161
+ }
162
+
49
163
  /**
50
164
  * Vuetify's square-inset switch hardcodes 6px and 4px, so a theme with no
51
165
  * corners at all still got a rounded toggle. Only this variant follows the
@@ -98,33 +212,24 @@
98
212
  }
99
213
 
100
214
  /* The group keeps the corners its children gave up. */
101
- & .v-btn-group.v-btn-group:not([class*='rounded-']) {
215
+ & .v-btn-group:not([class*='rounded-']) {
102
216
  border-radius: var(--v-vw-radius, 4px);
103
217
  }
104
218
 
105
219
  /**
106
- * Pagination items are `icon` buttons so the rule above leaves them
107
- * round, but they read as a row of tiles rather than icons — they follow
108
- * the button radius instead.
220
+ * Pagination items are `icon` buttons, but they read as a row of tiles
221
+ * rather than icons, so they follow the button radius. The rule above
222
+ * excludes them by name for that reason: two rules of equal weight
223
+ * claiming the same element would leave the outcome to source order,
224
+ * which is not something a stylesheet should have an opinion about.
109
225
  */
110
- & .v-pagination .v-btn.v-btn {
226
+ & .v-pagination .v-btn {
111
227
  border-radius: var(--v-vw-radius-btn, var(--v-vw-radius, 4px));
112
228
  }
113
229
 
114
- /**
115
- * The same containers join their buttons into one shape, so a per-button
116
- * shadow draws a seam down every join. Decoration belongs on the
117
- * container — this out-specifies the theme layers that give `.v-btn` a
118
- * relief, and a theme that wants a decorated group styles
119
- * `.v-btn-group` itself.
120
- */
121
- & :is(.v-btn-group, .v-toolbar-items) .v-btn.v-btn {
122
- box-shadow: none;
123
- }
124
-
125
230
  & :is(
126
- .v-card.v-card:not([class*='rounded-']),
127
- .v-table.v-table
231
+ .v-card:not([class*='rounded-']),
232
+ .v-table
128
233
  ) {
129
234
  border-radius: var(--v-vw-radius-lg, var(--v-vw-radius, 4px));
130
235
  }
@@ -135,14 +240,14 @@
135
240
  * to unlayered ones at any specificity, so styling `.v-expansion-panel`
136
241
  * here would flatten that logic and round every panel in the stack.
137
242
  */
138
- & .v-expansion-panels.v-expansion-panels {
243
+ & .v-expansion-panels {
139
244
  border-radius: var(--v-vw-radius-lg, var(--v-vw-radius, 4px));
140
245
  }
141
246
 
142
247
  & :is(
143
- .v-alert.v-alert:not([class*='rounded-']),
144
- .v-field.v-field:not([class*='rounded-']),
145
- .v-list-item.v-list-item:not([class*='rounded-']),
248
+ .v-alert:not([class*='rounded-']),
249
+ .v-field:not([class*='rounded-']),
250
+ .v-list-item:not([class*='rounded-']),
146
251
  .v-snackbar__wrapper,
147
252
  .v-menu > .v-overlay__content,
148
253
  .v-tooltip > .v-overlay__content,
@@ -152,11 +257,11 @@
152
257
  }
153
258
 
154
259
  /* Filled and underlined fields only round their top corners. */
155
- & .v-field.v-field:is(.v-field--variant-filled, .v-field--variant-underlined):not([class*='rounded-']) {
260
+ & .v-field:is(.v-field--variant-filled, .v-field--variant-underlined):not([class*='rounded-']) {
156
261
  border-radius: var(--v-vw-radius, 4px) var(--v-vw-radius, 4px) 0 0;
157
262
  }
158
263
 
159
- & .v-chip.v-chip:not([class*='rounded-']) {
264
+ & .v-chip:not([class*='rounded-']) {
160
265
  border-radius: var(--v-vw-radius-chip, 9999px);
161
266
  }
162
267
 
@@ -166,7 +271,7 @@
166
271
  * most themes even when nothing else is — only the square-cornered ones
167
272
  * set it.
168
273
  */
169
- & .v-slider-track.v-slider-track {
274
+ & .v-slider-track {
170
275
  border-radius: var(--v-vw-radius, 4px);
171
276
  }
172
277
 
@@ -211,17 +316,54 @@
211
316
  background-color: rgb(var(--v-theme-surface));
212
317
  }
213
318
 
319
+ /* --------------------------------------------------- nested surfaces */
320
+
321
+ /**
322
+ * A card is the surface; what it wraps is content. Vuetify paints a list
323
+ * or a table with `theme-surface` of its own, which matches only while the
324
+ * card is painted the same colour — so under a theme that defaults cards
325
+ * to outlined or tonal, or gives them a treatment of their own, the inner
326
+ * block reads as a pale slab that stops where its content does instead of
327
+ * filling the card.
328
+ *
329
+ * Direct children only, so a list inside `v-card-text` or a nested card
330
+ * keeps whatever it was given, and an explicit `bg-` still wins.
331
+ */
332
+ & .v-card > :is(.v-list, .v-table):not([class*='bg-']) {
333
+ background: transparent;
334
+ }
335
+
214
336
  /* -------------------------------------------------------- border width */
215
337
 
216
338
  & :is(
217
339
  .v-btn--variant-outlined,
218
340
  .v-card--variant-outlined,
219
341
  .v-alert--variant-outlined,
220
- .v-chip--variant-outlined
342
+ .v-chip--variant-outlined,
343
+ .v-list--border,
344
+ .v-sheet--border
221
345
  ) {
222
346
  border-width: var(--v-vw-border-width, thin);
223
347
  }
224
348
 
349
+ /**
350
+ * Vuetify draws an outlined card with `border: thin solid currentColor`,
351
+ * so the line around a panel is the ink of the text inside it — near
352
+ * black at high emphasis, measured. Every system this package is held
353
+ * against draws that line far lighter: Ant Design separates containers
354
+ * with #f0f0f0 and saves #d9d9d9 for controls.
355
+ *
356
+ * The line becomes the theme's own border colour instead. A theme that
357
+ * means its outline to be heavy — `brutalist` and its 3px rule — says so
358
+ * through the token rather than inheriting it by accident.
359
+ */
360
+ & .v-card--variant-outlined:not([class*='border-']) {
361
+ border-color: rgba(
362
+ var(--v-border-color),
363
+ var(--v-vw-outline-opacity, var(--v-border-opacity))
364
+ );
365
+ }
366
+
225
367
  /**
226
368
  * Vuetify already funnels every outline segment through one property, so
227
369
  * the field only needs that one set. Specificity matters here: this must
@@ -230,14 +372,29 @@
230
372
  */
231
373
  & .v-field__outline {
232
374
  --v-field-border-width: var(--v-vw-border-width, 1px);
375
+ /**
376
+ * Vuetify draws a resting field outline at 0.38 of the ink, which on a
377
+ * white ground reads around 2.3:1 — under the 3:1 WCAG asks for the
378
+ * boundary of a control you are meant to find and click. The value is
379
+ * declared on this element rather than inherited, so a theme cannot
380
+ * reach it through `variables` alone; the knob is here instead, and it
381
+ * defaults to Vuetify's own number so nothing moves unasked.
382
+ */
383
+ --v-field-border-opacity: var(--v-vw-field-border-opacity, 0.38);
233
384
  }
234
385
 
235
386
  /* ---------------------------------------------------- button typography */
236
387
 
237
- & .v-btn.v-btn {
388
+ /**
389
+ * The fallbacks are Vuetify 4's own values, not Material's older ones: v4
390
+ * dropped the uppercase button and the wide tracking that came with it,
391
+ * and a fallback that reintroduced them would be this file changing a
392
+ * theme that never asked it to.
393
+ */
394
+ & .v-btn {
238
395
  font-weight: var(--v-vw-btn-weight, 500);
239
- letter-spacing: var(--v-vw-btn-tracking, 0.0892857143em);
240
- text-transform: var(--v-vw-btn-transform, uppercase);
396
+ letter-spacing: var(--v-vw-btn-tracking, 0.0071428571em);
397
+ text-transform: var(--v-vw-btn-transform, none);
241
398
  }
242
399
 
243
400
  /* ------------------------------------------------- shared theme surfaces */
@@ -255,10 +412,231 @@
255
412
  box-shadow: var(--v-vw-overlay-shadow, revert-layer);
256
413
  }
257
414
 
415
+ /**
416
+ * A relief theme paints outside the shape it lifts, and a chip group is a
417
+ * scroller: it clips to its content box, so the glow around the chips
418
+ * comes out sheared along a straight edge with a corner where the row
419
+ * ends. The room has to be inside the clip to do any good, so the themes
420
+ * that lift their controls say how much the relief needs and the scroller
421
+ * carries it as padding.
422
+ */
423
+ & .v-chip-group .v-slide-group__content {
424
+ padding: var(--v-vw-relief-inset, 0);
425
+ }
426
+
427
+ /**
428
+ * The timeline's connecting line is structure, not decoration — it is the
429
+ * only thing saying the items are one sequence, so it cannot be left to
430
+ * `--v-border-opacity` alone. The two relief themes hold that variable at
431
+ * 0.001 so no stray rule can draw an outline anywhere, and the line went
432
+ * with it; they raise this one instead and get the line back without a
433
+ * single border returning.
434
+ */
435
+ /**
436
+ * Vuetify paints a skeleton's bones with `--v-border-opacity`, which ties
437
+ * "how visible is a loading placeholder" to "how heavy are this theme's
438
+ * borders". A theme that draws no borders at all — `clay` and `morph` hold
439
+ * the value at 0.001 so no stray outline can appear — loses its loading
440
+ * state entirely — while a theme whose borders are solid black paints the
441
+ * bones solid black, which is a filled box rather than a placeholder.
442
+ * Clamping both ends keeps the loading state legible in either direction
443
+ * without touching a single border, and a theme can still overrule it.
444
+ *
445
+ * The list is Vuetify's own: `__bone` is the layout class every piece
446
+ * carries, containers included, so painting that one fills the whole
447
+ * block instead of drawing bars on it.
448
+ */
449
+ & :is(
450
+ .v-skeleton-loader__avatar,
451
+ .v-skeleton-loader__button,
452
+ .v-skeleton-loader__chip,
453
+ .v-skeleton-loader__divider,
454
+ .v-skeleton-loader__heading,
455
+ .v-skeleton-loader__image,
456
+ .v-skeleton-loader__ossein,
457
+ .v-skeleton-loader__text
458
+ ) {
459
+ background: rgba(
460
+ var(--v-theme-on-surface),
461
+ var(--v-vw-skeleton-opacity, clamp(0.08, var(--v-border-opacity), 0.2))
462
+ );
463
+ }
464
+
465
+ /**
466
+ * The pieces carry hardcoded pill corners — 12px on a heading, 16 on a
467
+ * chip, 6 on a line of text — so a theme with square corners loads
468
+ * rounded bars and a theme with generous ones loads tight bars. A
469
+ * placeholder should be the shape of the thing it stands in for.
470
+ */
471
+ & :is(
472
+ .v-skeleton-loader__button,
473
+ .v-skeleton-loader__chip,
474
+ .v-skeleton-loader__heading,
475
+ .v-skeleton-loader__image,
476
+ .v-skeleton-loader__ossein,
477
+ .v-skeleton-loader__text
478
+ ) {
479
+ border-radius: var(--v-vw-radius, 4px);
480
+ }
481
+
482
+ & .v-skeleton-loader__avatar {
483
+ border-radius: var(--v-vw-radius-avatar, 50%);
484
+ }
485
+
486
+ /**
487
+ * The month and year controls are a fixed-width row inside a fixed-width
488
+ * card, so a theme that widens its buttons — wide tracking, a larger
489
+ * size — or narrows the picker pushes them past the edge. Wrapping is the
490
+ * one outcome that neither clips them nor forces a scrollbar.
491
+ */
492
+ /**
493
+ * The month and year buttons are navigation chrome inside a fixed-width
494
+ * card, not the theme's own buttons: a theme that sets wide tracking or
495
+ * capitals for its buttons — `lux` runs 0.16em — pushes them onto a
496
+ * second row, and a picker with two rows of navigation reads as broken.
497
+ * The type here follows the picker, not the theme's button voice.
498
+ */
499
+ & .v-date-picker-controls .v-btn {
500
+ letter-spacing: normal;
501
+ text-transform: none;
502
+ /* And not the theme's button size either: `candy` ships `size="large"`,
503
+ whose padding and minimum width push the pair past the card just as
504
+ tracking does. The row is chrome, so it keeps the picker's own size. */
505
+ font-size: 0.875rem;
506
+ min-width: 0;
507
+ padding-inline: 8px;
508
+ --v-btn-height: 32px;
509
+ }
510
+
511
+ & .v-date-picker-controls {
512
+ flex-wrap: wrap;
513
+ row-gap: 4px;
514
+ /* Vuetify pins the row at 56px, so a wrapped second line lands on the
515
+ weekday letters underneath. The height has to follow the content the
516
+ moment wrapping is allowed at all. */
517
+ height: auto;
518
+ min-height: var(--v-date-picker-controls-height, 56px);
519
+ padding-inline: 8px;
520
+ }
521
+
522
+ & :is(.v-timeline-divider__before, .v-timeline-divider__after) {
523
+ background: var(
524
+ --v-vw-timeline-line,
525
+ rgba(var(--v-border-color), var(--v-vw-timeline-line-opacity, var(--v-border-opacity)))
526
+ );
527
+ }
528
+
529
+ /**
530
+ * The whole background is a knob, not just its opacity, because a line is
531
+ * the one place a theme can say something with almost nothing: a
532
+ * repeating gradient makes it dashed, `--v-vw-gradient` makes it
533
+ * iridescent, a flat colour makes it a rule.
534
+ *
535
+ * The thickness is set on the segment rather than through Vuetify's
536
+ * `--v-timeline-line-thickness`, which the component writes inline from
537
+ * its own prop and would win.
538
+ */
539
+ &.v-application .v-timeline--vertical :is(.v-timeline-divider__before, .v-timeline-divider__after) {
540
+ width: var(--v-vw-timeline-line-width, var(--v-timeline-line-thickness));
541
+ }
542
+
543
+ &.v-application .v-timeline--horizontal :is(.v-timeline-divider__before, .v-timeline-divider__after) {
544
+ height: var(--v-vw-timeline-line-width, var(--v-timeline-line-thickness));
545
+ }
546
+
258
547
  & .v-tab__slider {
259
548
  height: var(--v-vw-tab-slider-height, 2px);
260
549
  }
261
550
 
551
+ /**
552
+ * A tab is a label, not a button. Vuetify builds one out of the other, so
553
+ * a tab inherits whatever the button type is — which in this build is the
554
+ * body size, since the reset layer's `font-size: inherit` on `button`
555
+ * outranks the size class that meant to set 0.875rem. The label ends up a
556
+ * step larger than the text it introduces.
557
+ *
558
+ * Material's tab label is 0.875rem and so is the button size Vuetify
559
+ * intends, so the value is the same either way and the tab no longer
560
+ * depends on which order the two layers happened to load in.
561
+ */
562
+ & .v-tab {
563
+ font-size: var(--v-vw-tab-size, 0.875rem);
564
+ }
565
+
566
+ /* -------------------------------------------------------- avatar type */
567
+
568
+ /**
569
+ * Vuetify sizes the circle and leaves the letters inside it at whatever
570
+ * the surrounding text was: `RM` in a 28px avatar next to a button label
571
+ * comes out 16px, more than half the diameter. Material's own ratio is
572
+ * 0.4 — 16px inside the 40px default — so this changes nothing at the
573
+ * default size and only corrects the ones a size prop moved.
574
+ *
575
+ * Only the keyword sizes publish their height as a variable; a numeric
576
+ * `size` is an inline style no rule can read, and the guard on the class
577
+ * keeps the declaration from resolving against a missing variable there.
578
+ * Images and icons carry their own dimensions and are left alone.
579
+ */
580
+ & .v-avatar[class*='--size-']:not(:has(img, .v-img, .v-icon)) {
581
+ font-size: calc(var(--v-avatar-height) * var(--v-vw-avatar-ratio, 0.4));
582
+ }
583
+
584
+ /* ------------------------------------------------------------ stepper */
585
+
586
+ /**
587
+ * The stepper is a panel, and Vuetify raises it twice: an elevation on
588
+ * the sheet, and a second one under the header band. That is Material's
589
+ * way of separating a scrolling window from its chrome, and no theme here
590
+ * separates anything that way — the flat ones had already dropped the
591
+ * sheet's shadow through their `.v-sheet` rule and were left with a band
592
+ * floating inside a panel that casts nothing.
593
+ *
594
+ * So the panel takes the theme's own edge and the header separates with
595
+ * the theme's own hairline — through the panel tokens above, so a theme
596
+ * that decorates its panels decorates its stepper by saying nothing
597
+ * twice: brutalist's second shape and clay's relief arrive here on their
598
+ * own. What is left is the fallback for a theme with no opinion, which is
599
+ * a hairline and no shadow.
600
+ */
601
+ & .v-stepper.v-sheet:not(.v-stepper--flat, [class*='elevation-']) {
602
+ box-shadow: var(--v-vw-panel-shadow, none);
603
+ border: var(--v-vw-panel-border, var(--v-vw-border-width, thin) solid
604
+ rgba(var(--v-border-color), var(--v-vw-outline-opacity, var(--v-border-opacity))));
605
+ }
606
+
607
+ & .v-stepper .v-stepper-header {
608
+ box-shadow: none;
609
+ border-block-end: var(--v-vw-border-width, thin) solid
610
+ rgba(var(--v-border-color), var(--v-vw-outline-opacity, var(--v-border-opacity)));
611
+ }
612
+
613
+ /**
614
+ * And the steps themselves come down a size. Vuetify pads each one by
615
+ * 1.5rem on every side, which makes a three-word progress bar 72px tall —
616
+ * taller than the app bar above it — and margins the window by as much
617
+ * again. This is the one component where the stock metrics are set for a
618
+ * full-page wizard and every other use is a form inside a panel.
619
+ */
620
+ & :where(.v-stepper-item) {
621
+ padding: 1rem 1.25rem;
622
+ }
623
+
624
+ & :where(.v-stepper-window) {
625
+ margin: 1.25rem;
626
+ }
627
+
628
+ & :where(.v-stepper .v-stepper-actions) {
629
+ padding: 0 1.25rem 1rem;
630
+ }
631
+
632
+ /**
633
+ * Figures line up in a column only when every digit is the same width,
634
+ * which matters on any screen that is mostly numbers and nowhere else.
635
+ */
636
+ & :is(.v-table, .v-data-table, .v-pagination) {
637
+ font-variant-numeric: var(--v-vw-numeric, normal);
638
+ }
639
+
262
640
  & :is(.v-table thead th, .v-data-table__th) {
263
641
  font-weight: var(--v-vw-th-weight, revert-layer);
264
642
  letter-spacing: var(--v-vw-th-tracking, revert-layer);
@@ -274,6 +652,70 @@
274
652
  border-inline-start: var(--v-vw-list-bar, 0) solid var(--v-vw-list-bar-color, currentColor);
275
653
  }
276
654
 
655
+ /* -------------------------------------------------------------- motion */
656
+
657
+ /**
658
+ * Movement is the fourth axis a theme travels on, after colour, shape and
659
+ * type — `brutalist` snapping in two steps and `lux` easing over a
660
+ * quarter-second are as different as their palettes, and until now every
661
+ * theme moved like stock Vuetify unless it hand-rolled a rule.
662
+ *
663
+ * Themes state a character in four values and core spends them on the
664
+ * same interactions everywhere. Every selector here is wrapped in
665
+ * `:where()`, so its specificity is the theme class alone and any theme
666
+ * that wants its own move — brutalist sliding into its shadow, candy
667
+ * squashing — overrides this without a fight.
668
+ *
669
+ * Only `transform` is animated: it runs on the compositor, so none of
670
+ * this can cost a layout pass however deep the tree.
671
+ */
672
+ & :where(.v-btn, .v-chip, .v-list-item, .v-card, .v-tab) {
673
+ transition: transform var(--v-vw-motion-duration, 160ms) var(--v-vw-motion-ease, cubic-bezier(0.2, 0, 0.2, 1));
674
+ }
675
+
676
+ & :where(.v-btn, .v-chip--link, .v-list-item--link):not(.v-btn--disabled):active {
677
+ transform:
678
+ translateY(var(--v-vw-press-shift, 0px))
679
+ scale(var(--v-vw-press-scale, 0.98));
680
+ }
681
+
682
+ & :where(.v-card--link):hover {
683
+ transform: translateY(calc(-1 * var(--v-vw-hover-lift, 0px)));
684
+ }
685
+
686
+ /**
687
+ * The pop: a control that has just been switched on overshoots its own
688
+ * size and settles back, which is the whole reason a like button feels
689
+ * like anything at all.
690
+ *
691
+ * It rides on the state classes Vuetify already toggles, so it costs no
692
+ * script and no markup. The tradeoff is that a control rendered already
693
+ * checked pops once on mount — a form arriving with its defaults ticking
694
+ * in, which reads as intent rather than as a glitch. A theme that would
695
+ * rather not can set `vw-pop-duration: 0ms`.
696
+ */
697
+ & :where(
698
+ .v-selection-control--dirty .v-selection-control__input > .v-icon,
699
+ .v-btn--active > .v-btn__content > .v-icon,
700
+ .v-rating__item--full .v-icon
701
+ ) {
702
+ animation: vw-pop var(--v-vw-pop-duration, 220ms) var(--v-vw-motion-ease, cubic-bezier(0.2, 0, 0.2, 1));
703
+ }
704
+
705
+ /* --------------------------------------------------------------- focus */
706
+
707
+ /**
708
+ * Vuetify marks focus with a wash of `primary` at low opacity, which
709
+ * disappears on a dark surface and on any element already carrying a
710
+ * colour. A ring does not: it is drawn outside the shape, so it survives
711
+ * every background, and it is the one piece of the interface that has to
712
+ * work for someone who never touches a mouse.
713
+ */
714
+ & :where(.v-btn, .v-tab, .v-chip, .v-list-item, .v-selection-control__input):focus-visible {
715
+ outline: var(--v-vw-focus-ring, 2px solid rgb(var(--v-theme-primary)));
716
+ outline-offset: var(--v-vw-focus-offset, 2px);
717
+ }
718
+
277
719
  /* ------------------------------------------------------------ headings */
278
720
 
279
721
  /**
@@ -309,3 +751,89 @@
309
751
  text-underline-offset: 0.15em;
310
752
  }
311
753
  }
754
+
755
+ @keyframes vw-pop {
756
+ 0% {
757
+ transform: scale(1);
758
+ }
759
+
760
+ 45% {
761
+ transform: scale(var(--v-vw-pop-scale, 1.18));
762
+ }
763
+
764
+ 100% {
765
+ transform: scale(1);
766
+ }
767
+ }
768
+
769
+ /**
770
+ * Corrections, and the one place weight runs the other way.
771
+ *
772
+ * Everything above is a default a theme is free to overrule. The rules
773
+ * below are the opposite: they exist because a theme's own rule does damage
774
+ * in a context it never considered, so they keep the scope's weight and
775
+ * outrank it deliberately. Anything added here needs that justification —
776
+ * otherwise it belongs in the weightless block above.
777
+ */
778
+ :is([data-vuetiwatch]:not(VUETIWATCH_STOCK), .v-theme--VUETIWATCH_THEMES) {
779
+ /**
780
+ * A button group and a toolbar join their buttons into one shape, so a
781
+ * per-button shadow draws a seam down every join — visible on any theme
782
+ * that gives `.v-btn` a relief or a glow. Decoration belongs on the
783
+ * container, and a theme that wants a decorated group styles
784
+ * `.v-btn-group` itself.
785
+ */
786
+ & :is(.v-btn-group, .v-toolbar-items) .v-btn.v-btn {
787
+ box-shadow: none;
788
+ }
789
+ }
790
+
791
+ /**
792
+ * One switch for every theme at once: the tokens themselves go to zero, so
793
+ * anything driven by them stops moving — including a theme built with
794
+ * `defineTheme()` that never wrote a media query of its own.
795
+ */
796
+ @media (prefers-reduced-motion: reduce) {
797
+ :where([data-vuetiwatch]:not(VUETIWATCH_STOCK), .v-theme--VUETIWATCH_THEMES) {
798
+ --v-vw-motion-duration: 1ms;
799
+ --v-vw-pop-duration: 1ms;
800
+ --v-vw-press-scale: 1;
801
+ --v-vw-press-shift: 0px;
802
+ --v-vw-hover-lift: 0px;
803
+ --v-vw-theme-transition: 0ms;
804
+ }
805
+ }
806
+
807
+ /**
808
+ * Theme switching, as a transition rather than a jump.
809
+ *
810
+ * `createVuetiwatch()` mirrors the active theme onto `<html>`, which is the
811
+ * same element the view-transition pseudo-elements hang off — so these
812
+ * rules reach exactly the documents running a Vuetiwatch theme and nothing
813
+ * else. The plugin writes the click coordinates into `--vw-origin-*` before
814
+ * it starts the transition, so the new theme opens from the switch the user
815
+ * actually pressed; with no event to read, it opens from the centre.
816
+ *
817
+ * The old snapshot is held still on purpose. Cross-fading both halves turns
818
+ * two legible screens into one muddy one, while wiping the new theme over a
819
+ * frozen old one keeps every edge crisp for the whole animation.
820
+ */
821
+ @keyframes vw-theme-wipe {
822
+ from {
823
+ clip-path: circle(0% at var(--vw-origin-x, 50%) var(--vw-origin-y, 50%));
824
+ }
825
+
826
+ to {
827
+ clip-path: circle(150% at var(--vw-origin-x, 50%) var(--vw-origin-y, 50%));
828
+ }
829
+ }
830
+
831
+ html[data-vuetiwatch]::view-transition-old(root) {
832
+ animation: none;
833
+ }
834
+
835
+ html[data-vuetiwatch]::view-transition-new(root) {
836
+ animation: vw-theme-wipe
837
+ var(--v-vw-theme-transition, 420ms)
838
+ var(--v-vw-motion-ease, cubic-bezier(0.2, 0, 0.2, 1));
839
+ }