@nonsuch/component-library 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,9 +12,9 @@ pnpm add @nonsuch/component-library
12
12
  pnpm add quasar @quasar/extras @quasar/vite-plugin
13
13
  ```
14
14
 
15
- ### Vite Configuration
15
+ ### Quick Start (Recommended)
16
16
 
17
- In your `vite.config.ts`, register the Quasar Vite plugin as usual:
17
+ #### Vite Configuration
18
18
 
19
19
  ```ts
20
20
  import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
@@ -25,25 +25,89 @@ export default defineConfig({
25
25
  })
26
26
  ```
27
27
 
28
- ### App Entry
29
-
30
- Register Quasar in your Vue app:
28
+ #### App Entry
31
29
 
32
30
  ```ts
33
31
  import { createApp } from 'vue'
34
32
  import { Quasar } from 'quasar'
33
+ import { createNonsuch, createQuasarConfig } from '@nonsuch/component-library'
34
+ import '@nonsuch/component-library/tokens.css'
35
35
  import 'quasar/src/css/index.sass'
36
36
 
37
37
  const app = createApp(App)
38
- app.use(Quasar, { plugins: {} })
38
+ app.use(Quasar, createQuasarConfig()) // Token-aligned Quasar brand colours
39
+ app.use(createNonsuch()) // Locale + library setup
40
+ app.mount('#app')
41
+ ```
42
+
43
+ That's it — components, tokens, locale (defaults to `en-CA`), and Quasar brand colours are all wired up.
44
+
45
+ #### Use Components
46
+
47
+ ```vue
48
+ <script setup>
49
+ import { NsButton, NsInput, NsCard } from '@nonsuch/component-library'
50
+ </script>
51
+
52
+ <template>
53
+ <NsCard title="Welcome">
54
+ <NsInput v-model="name" label="Your name" />
55
+ <template #actions>
56
+ <NsButton label="Submit" />
57
+ </template>
58
+ </NsCard>
59
+ </template>
60
+ ```
61
+
62
+ ### Plugin Options
63
+
64
+ `createNonsuch()` accepts options for locale:
65
+
66
+ ```ts
67
+ import { createNonsuch, nsLocaleFrCA } from '@nonsuch/component-library'
68
+
69
+ app.use(createNonsuch({ locale: nsLocaleFrCA }))
70
+ ```
71
+
72
+ `createQuasarConfig()` accepts brand colour overrides and extra Quasar config:
73
+
74
+ ```ts
75
+ import { createQuasarConfig } from '@nonsuch/component-library'
76
+
77
+ app.use(Quasar, createQuasarConfig({
78
+ brand: { primary: '#1a73e8' },
79
+ plugins: { Notify: {} },
80
+ }))
39
81
  ```
40
82
 
41
- Then import custom components as needed:
83
+ ### Dark Mode
42
84
 
43
85
  ```ts
44
- import { NsButton } from '@nonsuch/component-library'
86
+ import { useNsDarkMode } from '@nonsuch/component-library'
87
+
88
+ const { isDark, toggle, useSystem } = useNsDarkMode()
45
89
  ```
46
90
 
91
+ The composable persists the user's choice to `localStorage` and syncs with `prefers-color-scheme`. Design tokens switch automatically via the `dark` class on `<html>`.
92
+
93
+ ### NsThemeProvider
94
+
95
+ For section-level locale overrides without a plugin:
96
+
97
+ ```vue
98
+ <script setup>
99
+ import { NsThemeProvider, nsLocaleFrCA } from '@nonsuch/component-library'
100
+ </script>
101
+
102
+ <template>
103
+ <NsThemeProvider :locale="nsLocaleFrCA">
104
+ <!-- All Ns components here use French strings -->
105
+ </NsThemeProvider>
106
+ </template>
107
+ ```
108
+
109
+ ### Manual Setup (Advanced)
110
+
47
111
  ### Fonts (Optional)
48
112
 
49
113
  The library ships [Fixel](https://fixel.macpaw.com/) as the Nonsuch brand font with Roboto as a fallback. Three integration options:
@@ -86,6 +150,77 @@ Or use Quasar components directly — they aren't re-exported through this libra
86
150
  import { QInput, QSelect } from 'quasar'
87
151
  ```
88
152
 
153
+ ### Translations (i18n)
154
+
155
+ The library ships its own locale system — **no dependency on vue-i18n**. Components that render user-visible text accept optional string props with built-in defaults from the active locale.
156
+
157
+ **Built-in locales:** `en-CA` (default) and `fr-CA`.
158
+
159
+ **Option 1: Use the defaults** — components use English (Canada) strings out of the box with no setup:
160
+
161
+ ```vue
162
+ <NsButton>Add to cart</NsButton>
163
+ <!-- internal labels like loading text already default to English -->
164
+ ```
165
+
166
+ **Option 2: Switch locale globally** — provide a locale pack at the app root:
167
+
168
+ ```ts
169
+ import { createApp } from 'vue'
170
+ import { provideNsLocale, nsLocaleFrCA } from '@nonsuch/component-library'
171
+
172
+ const app = createApp(App)
173
+
174
+ // Inside your root component's setup():
175
+ provideNsLocale(nsLocaleFrCA)
176
+ ```
177
+
178
+ **Option 3: Custom / partial locale** — supply your own translations by implementing the `NsLocaleMessages` interface:
179
+
180
+ ```ts
181
+ import type { NsLocaleMessages } from '@nonsuch/component-library'
182
+ import { nsLocaleEnCA, provideNsLocale } from '@nonsuch/component-library'
183
+
184
+ const myLocale: NsLocaleMessages = {
185
+ ...nsLocaleEnCA,
186
+ product: {
187
+ ...nsLocaleEnCA.product,
188
+ addToCart: 'Add to bag', // override just what you need
189
+ },
190
+ }
191
+
192
+ provideNsLocale(myLocale)
193
+ ```
194
+
195
+ **Option 4: Override per-component** — pass a string prop directly to bypass the locale:
196
+
197
+ ```vue
198
+ <!-- This label is always "Ajouter" regardless of the active locale -->
199
+ <NsButton label="Ajouter" />
200
+ ```
201
+
202
+ The locale interface covers four sections: `common`, `product`, `media`, and `validation`. See the full type in `NsLocaleMessages`.
203
+
204
+ ### Design Tokens (Optional)
205
+
206
+ Import CSS custom properties for colours, typography, spacing, border-radius, shadows, and motion:
207
+
208
+ ```ts
209
+ import '@nonsuch/component-library/tokens.css'
210
+ ```
211
+
212
+ All tokens use the `--ns-` prefix and support light/dark mode automatically. Current values are placeholders — token names are stable.
213
+
214
+ ```css
215
+ .my-card {
216
+ border-radius: var(--ns-radius-md);
217
+ box-shadow: var(--ns-shadow-sm);
218
+ padding: var(--ns-space-4);
219
+ }
220
+ ```
221
+
222
+ Dark mode activates via `class="dark"`, `data-theme="dark"`, Quasar's `.q-dark`, or `prefers-color-scheme: dark`.
223
+
89
224
  ## Development
90
225
 
91
226
  ```bash
@@ -110,13 +245,22 @@ pnpm build:storybook
110
245
 
111
246
  ```markdown
112
247
  src/
113
- index.ts # Library entry — exports all custom components
114
- components/
115
- NsButton/
116
- NsButton.vue # Component implementation
117
- NsButton.stories.ts # Storybook story
118
- NsButton.test.ts # Vitest unit test
119
- index.ts # Re-export
248
+ index.ts # Library entry — exports all public API
249
+ plugin.ts # createNonsuch() Vue plugin
250
+ quasarConfig.ts # createQuasarConfig() helper
251
+ components/
252
+ NsButton/ # Styled QBtn wrapper
253
+ NsCard/ # Card with title/subtitle/actions slots
254
+ NsInput/ # Styled QInput wrapper
255
+ NsSkeleton/ # Loading skeleton with animation
256
+ NsThemeProvider/ # Renderless locale provider
257
+ composables/
258
+ useNsLocale.ts # Locale injection/provision
259
+ useNsDarkMode.ts # Dark mode with persistence
260
+ useNsDefaults.ts # Default value helper
261
+ locale/ # en-CA, fr-CA string packs
262
+ tokens/ # Design token CSS + TS helpers
263
+ fonts/ # Fixel font files + CSS
120
264
  ```
121
265
 
122
266
  Each custom component lives in its own directory with co-located story and test files. The `Ns` prefix distinguishes library components from Quasar's `Q` prefix.
@@ -1 +1 @@
1
- .ns-button[data-v-e1d8675d]{font-family:Fixel Text,Roboto,sans-serif;font-weight:500;letter-spacing:.02em}
1
+ .ns-button[data-v-7f6c2760]{font-family:var(--ns-font-family-text);font-weight:var(--ns-font-weight-medium);letter-spacing:var(--ns-letter-spacing-wide)}.ns-skeleton[data-v-62627927]{border-radius:var(--ns-radius-md)}.ns-input[data-v-7ebf3284],.ns-input[data-v-7ebf3284] .q-field__label{font-family:var(--ns-font-family-text)}.ns-input[data-v-7ebf3284] .q-field__control{border-radius:var(--ns-radius-md)}.ns-card[data-v-41ecea50]{border-radius:var(--ns-radius-lg);box-shadow:var(--ns-shadow-sm);font-family:var(--ns-font-family-text);transition:box-shadow var(--ns-duration-normal) var(--ns-easing-default)}.ns-card[data-v-41ecea50]:hover{box-shadow:var(--ns-shadow-md)}.ns-card--flat[data-v-41ecea50],.ns-card--flat[data-v-41ecea50]:hover{box-shadow:none}.ns-card__header[data-v-41ecea50]{font-family:var(--ns-font-family-display)}
@@ -1,35 +1,354 @@
1
- import a from "quasar/src/components/btn/QBtn.js";
2
- import { defineComponent as d, openBlock as u, createBlock as l, mergeProps as s, withCtx as c, renderSlot as f } from "vue";
3
- const p = /* @__PURE__ */ d({
1
+ import Q from "quasar/src/components/btn/QBtn.js";
2
+ import { defineComponent as c, openBlock as s, createBlock as d, mergeProps as p, withCtx as u, renderSlot as i, createVNode as B, unref as T, provide as V, inject as q, createSlots as z, renderList as R, normalizeProps as D, guardReactiveProps as M, createElementVNode as O, toDisplayString as w, createElementBlock as F, createCommentVNode as v, computed as _, ref as C, onMounted as x, onUnmounted as j, readonly as I } from "vue";
3
+ import K from "quasar/src/components/spinner/QSpinnerDots.js";
4
+ import U from "quasar/src/components/skeleton/QSkeleton.js";
5
+ import Z from "quasar/src/components/input/QInput.js";
6
+ import $ from "quasar/src/components/card/QCardSection.js";
7
+ import G from "quasar/src/components/card/QCardActions.js";
8
+ import H from "quasar/src/components/card/QCard.js";
9
+ const Y = /* @__PURE__ */ c({
4
10
  __name: "NsButton",
5
11
  props: {
6
12
  color: { default: "primary" },
7
13
  size: { default: "md" },
8
14
  unelevated: { type: Boolean, default: !0 },
9
15
  noCaps: { type: Boolean, default: !0 },
10
- rounded: { type: Boolean, default: !0 }
16
+ rounded: { type: Boolean, default: !0 },
17
+ loading: { type: Boolean, default: !1 }
11
18
  },
12
19
  setup(e) {
13
- return (t, o) => (u(), l(a, s(t.$attrs, {
20
+ return (t, o) => (s(), d(Q, p(t.$attrs, {
14
21
  color: e.color,
15
22
  size: e.size,
16
23
  unelevated: e.unelevated,
17
24
  "no-caps": e.noCaps,
18
25
  rounded: e.rounded,
26
+ loading: e.loading,
19
27
  class: "ns-button"
20
28
  }), {
21
- default: c(() => [
22
- f(t.$slots, "default", {}, void 0, !0)
29
+ loading: u(() => [
30
+ i(t.$slots, "loading", {}, () => [
31
+ B(T(K), { color: "white" })
32
+ ], !0)
33
+ ]),
34
+ default: u(() => [
35
+ i(t.$slots, "default", {}, void 0, !0)
23
36
  ]),
24
37
  _: 3
25
- }, 16, ["color", "size", "unelevated", "no-caps", "rounded"]));
38
+ }, 16, ["color", "size", "unelevated", "no-caps", "rounded", "loading"]));
26
39
  }
27
- }), i = (e, t) => {
40
+ }), g = (e, t) => {
28
41
  const o = e.__vccOpts || e;
29
42
  for (const [n, r] of t)
30
43
  o[n] = r;
31
44
  return o;
32
- }, B = /* @__PURE__ */ i(p, [["__scopeId", "data-v-e1d8675d"]]);
45
+ }, me = /* @__PURE__ */ g(Y, [["__scopeId", "data-v-7f6c2760"]]), J = /* @__PURE__ */ c({
46
+ __name: "NsSkeleton",
47
+ props: {
48
+ type: { default: "rect" },
49
+ animation: { default: "wave" },
50
+ square: { type: Boolean, default: !1 },
51
+ bordered: { type: Boolean, default: !1 },
52
+ width: { default: void 0 },
53
+ height: { default: void 0 }
54
+ },
55
+ setup(e) {
56
+ return (t, o) => (s(), d(U, p(t.$attrs, {
57
+ type: e.type,
58
+ animation: e.animation,
59
+ square: e.square,
60
+ bordered: e.bordered,
61
+ width: e.width,
62
+ height: e.height,
63
+ class: "ns-skeleton"
64
+ }), null, 16, ["type", "animation", "square", "bordered", "width", "height"]));
65
+ }
66
+ }), pe = /* @__PURE__ */ g(J, [["__scopeId", "data-v-62627927"]]), y = {
67
+ common: {
68
+ loading: "Loading…",
69
+ retry: "Retry",
70
+ cancel: "Cancel",
71
+ confirm: "Confirm",
72
+ save: "Save",
73
+ delete: "Delete",
74
+ edit: "Edit",
75
+ search: "Search",
76
+ noResults: "No results found",
77
+ showMore: "Show more",
78
+ showLess: "Show less"
79
+ },
80
+ product: {
81
+ addToCart: "Add to cart",
82
+ outOfStock: "Out of stock",
83
+ inStock: "In stock",
84
+ quantity: "Quantity",
85
+ price: "Price",
86
+ sale: "Sale"
87
+ },
88
+ media: {
89
+ zoomIn: "Zoom in",
90
+ zoomOut: "Zoom out",
91
+ fullscreen: "Fullscreen",
92
+ exitFullscreen: "Exit fullscreen",
93
+ previousImage: "Previous image",
94
+ nextImage: "Next image"
95
+ },
96
+ validation: {
97
+ required: "This field is required",
98
+ invalidEmail: "Please enter a valid email address",
99
+ tooShort: "Too short",
100
+ tooLong: "Too long"
101
+ }
102
+ }, S = /* @__PURE__ */ Symbol("ns-locale");
103
+ function W(e) {
104
+ V(S, e);
105
+ }
106
+ function X() {
107
+ return q(S, y);
108
+ }
109
+ const ge = /* @__PURE__ */ c({
110
+ __name: "NsThemeProvider",
111
+ props: {
112
+ locale: { default: () => y }
113
+ },
114
+ setup(e) {
115
+ return W(e.locale), (o, n) => i(o.$slots, "default");
116
+ }
117
+ }), ee = /* @__PURE__ */ c({
118
+ __name: "NsInput",
119
+ props: {
120
+ label: { default: void 0 },
121
+ modelValue: { default: void 0 },
122
+ outlined: { type: Boolean, default: !0 },
123
+ dense: { type: Boolean, default: !1 },
124
+ rules: { default: void 0 }
125
+ },
126
+ emits: ["update:modelValue"],
127
+ setup(e) {
128
+ return (t, o) => (s(), d(Z, p(t.$attrs, {
129
+ "model-value": e.modelValue,
130
+ label: e.label,
131
+ outlined: e.outlined,
132
+ dense: e.dense,
133
+ rules: e.rules,
134
+ class: "ns-input",
135
+ "onUpdate:modelValue": o[0] || (o[0] = (n) => t.$emit("update:modelValue", n))
136
+ }), z({ _: 2 }, [
137
+ R(t.$slots, (n, r) => ({
138
+ name: r,
139
+ fn: u((l) => [
140
+ i(t.$slots, r, D(M(l ?? {})), void 0, !0)
141
+ ])
142
+ }))
143
+ ]), 1040, ["model-value", "label", "outlined", "dense", "rules"]));
144
+ }
145
+ }), ve = /* @__PURE__ */ g(ee, [["__scopeId", "data-v-7ebf3284"]]), te = { class: "text-h6" }, oe = {
146
+ key: 0,
147
+ class: "text-subtitle2 text-grey"
148
+ }, ne = /* @__PURE__ */ c({
149
+ __name: "NsCard",
150
+ props: {
151
+ title: { default: void 0 },
152
+ subtitle: { default: void 0 },
153
+ flat: { type: Boolean, default: !1 }
154
+ },
155
+ setup(e) {
156
+ return (t, o) => (s(), d(H, p(t.$attrs, {
157
+ class: ["ns-card", { "ns-card--flat": e.flat }]
158
+ }), {
159
+ default: u(() => [
160
+ e.title || t.$slots.header ? (s(), d($, {
161
+ key: 0,
162
+ class: "ns-card__header"
163
+ }, {
164
+ default: u(() => [
165
+ i(t.$slots, "header", {}, () => [
166
+ O("div", te, w(e.title), 1),
167
+ e.subtitle ? (s(), F("div", oe, w(e.subtitle), 1)) : v("", !0)
168
+ ], !0)
169
+ ]),
170
+ _: 3
171
+ })) : v("", !0),
172
+ B($, null, {
173
+ default: u(() => [
174
+ i(t.$slots, "default", {}, void 0, !0)
175
+ ]),
176
+ _: 3
177
+ }),
178
+ t.$slots.actions ? (s(), d(G, {
179
+ key: 1,
180
+ align: "right"
181
+ }, {
182
+ default: u(() => [
183
+ i(t.$slots, "actions", {}, void 0, !0)
184
+ ]),
185
+ _: 3
186
+ })) : v("", !0)
187
+ ]),
188
+ _: 3
189
+ }, 16, ["class"]));
190
+ }
191
+ }), he = /* @__PURE__ */ g(ne, [["__scopeId", "data-v-41ecea50"]]);
192
+ function ye(e = {}) {
193
+ const { locale: t = y } = e;
194
+ return {
195
+ install(o) {
196
+ o.provide(S, t);
197
+ }
198
+ };
199
+ }
200
+ const re = {
201
+ primary: "#3b82f6",
202
+ // PLACEHOLDER — matches --ns-color-primary
203
+ secondary: "#8b5cf6",
204
+ // PLACEHOLDER — matches --ns-color-secondary
205
+ accent: "#f59e0b",
206
+ // PLACEHOLDER — matches --ns-color-accent
207
+ dark: "#1e293b",
208
+ // PLACEHOLDER — matches --ns-color-neutral-800
209
+ "dark-page": "#0f172a",
210
+ // PLACEHOLDER — matches --ns-color-neutral-900
211
+ positive: "#22c55e",
212
+ // PLACEHOLDER — matches --ns-color-success
213
+ negative: "#ef4444",
214
+ // PLACEHOLDER — matches --ns-color-error
215
+ info: "#3b82f6",
216
+ // PLACEHOLDER — matches --ns-color-info
217
+ warning: "#f59e0b"
218
+ // PLACEHOLDER — matches --ns-color-warning
219
+ };
220
+ function Se(e = {}) {
221
+ const { brand: t = {}, plugins: o = {}, ...n } = e;
222
+ return {
223
+ config: {
224
+ brand: {
225
+ ...re,
226
+ ...t
227
+ }
228
+ },
229
+ plugins: o,
230
+ ...n
231
+ };
232
+ }
233
+ const ke = {
234
+ common: {
235
+ loading: "Chargement…",
236
+ retry: "Réessayer",
237
+ cancel: "Annuler",
238
+ confirm: "Confirmer",
239
+ save: "Enregistrer",
240
+ delete: "Supprimer",
241
+ edit: "Modifier",
242
+ search: "Rechercher",
243
+ noResults: "Aucun résultat trouvé",
244
+ showMore: "Afficher plus",
245
+ showLess: "Afficher moins"
246
+ },
247
+ product: {
248
+ addToCart: "Ajouter au panier",
249
+ outOfStock: "Rupture de stock",
250
+ inStock: "En stock",
251
+ quantity: "Quantité",
252
+ price: "Prix",
253
+ sale: "Solde"
254
+ },
255
+ media: {
256
+ zoomIn: "Agrandir",
257
+ zoomOut: "Réduire",
258
+ fullscreen: "Plein écran",
259
+ exitFullscreen: "Quitter le plein écran",
260
+ previousImage: "Image précédente",
261
+ nextImage: "Image suivante"
262
+ },
263
+ validation: {
264
+ required: "Ce champ est requis",
265
+ invalidEmail: "Veuillez entrer une adresse courriel valide",
266
+ tooShort: "Trop court",
267
+ tooLong: "Trop long"
268
+ }
269
+ };
270
+ function be(e, t) {
271
+ const o = X();
272
+ return _(() => {
273
+ const n = e();
274
+ if (n != null) return n;
275
+ const r = t.split(".");
276
+ let l = o;
277
+ for (const f of r)
278
+ l = l[f];
279
+ return l;
280
+ });
281
+ }
282
+ const h = "ns-dark-mode";
283
+ function Ne() {
284
+ const e = C(!1), t = C("system");
285
+ let o = null, n = null;
286
+ function r(a) {
287
+ if (e.value = a, typeof document < "u") {
288
+ const m = document.documentElement;
289
+ m.classList.toggle("dark", a), m.setAttribute("data-theme", a ? "dark" : "light");
290
+ }
291
+ }
292
+ function l() {
293
+ return typeof window > "u" ? !1 : window.matchMedia("(prefers-color-scheme: dark)").matches;
294
+ }
295
+ function f() {
296
+ if (typeof localStorage > "u") return null;
297
+ const a = localStorage.getItem(h);
298
+ return a === "true" ? !0 : a === "false" ? !1 : null;
299
+ }
300
+ function k(a) {
301
+ typeof localStorage < "u" && localStorage.setItem(h, String(a));
302
+ }
303
+ function E() {
304
+ typeof localStorage < "u" && localStorage.removeItem(h);
305
+ }
306
+ function b() {
307
+ t.value = "user", k(!0), r(!0);
308
+ }
309
+ function N() {
310
+ t.value = "user", k(!1), r(!1);
311
+ }
312
+ function L() {
313
+ e.value ? N() : b();
314
+ }
315
+ function A() {
316
+ E(), t.value = "system", r(l());
317
+ }
318
+ function P() {
319
+ const a = f();
320
+ a !== null ? (t.value = "storage", r(a)) : (t.value = "system", r(l())), typeof window < "u" && (o = window.matchMedia("(prefers-color-scheme: dark)"), n = (m) => {
321
+ f() === null && (t.value = "system", r(m.matches));
322
+ }, o.addEventListener("change", n));
323
+ }
324
+ return x(P), j(() => {
325
+ o && n && o.removeEventListener("change", n);
326
+ }), {
327
+ isDark: I(e),
328
+ source: I(t),
329
+ enable: b,
330
+ disable: N,
331
+ toggle: L,
332
+ useSystem: A
333
+ };
334
+ }
335
+ function we(e, t = document.documentElement) {
336
+ return getComputedStyle(t).getPropertyValue(e).trim();
337
+ }
33
338
  export {
34
- B as NsButton
339
+ me as NsButton,
340
+ he as NsCard,
341
+ ve as NsInput,
342
+ S as NsLocaleKey,
343
+ pe as NsSkeleton,
344
+ ge as NsThemeProvider,
345
+ ye as createNonsuch,
346
+ Se as createQuasarConfig,
347
+ we as getToken,
348
+ y as nsLocaleEnCA,
349
+ ke as nsLocaleFrCA,
350
+ W as provideNsLocale,
351
+ Ne as useNsDarkMode,
352
+ be as useNsDefault,
353
+ X as useNsLocale
35
354
  };
package/fonts/global.css CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  /* Apply Fixel Text as the default body font */
16
16
  body {
17
- font-family: 'Fixel Text', 'Roboto', sans-serif;
17
+ font-family: var(--ns-font-family-text, 'Fixel Text', 'Roboto', sans-serif);
18
18
  }
19
19
 
20
20
  /* Apply Fixel Display to headings */
@@ -30,5 +30,5 @@ h6,
30
30
  .text-h4,
31
31
  .text-h5,
32
32
  .text-h6 {
33
- font-family: 'Fixel Display', 'Roboto', sans-serif;
33
+ font-family: var(--ns-font-family-display, 'Fixel Display', 'Roboto', sans-serif);
34
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nonsuch/component-library",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "A Vue 3 component library built on Quasar with opinionated defaults and custom components.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -11,7 +11,8 @@
11
11
  },
12
12
  "files": [
13
13
  "dist",
14
- "fonts"
14
+ "fonts",
15
+ "src/tokens/tokens.css"
15
16
  ],
16
17
  "main": "./dist/nonsuch-components.js",
17
18
  "module": "./dist/nonsuch-components.js",
@@ -24,7 +25,8 @@
24
25
  "./style.css": "./dist/style.css",
25
26
  "./fonts.css": "./fonts/fonts.css",
26
27
  "./fonts/global.css": "./fonts/global.css",
27
- "./fonts/quasar-overrides": "./fonts/_quasar-overrides.sass"
28
+ "./fonts/quasar-overrides": "./fonts/_quasar-overrides.sass",
29
+ "./tokens.css": "./src/tokens/tokens.css"
28
30
  },
29
31
  "scripts": {
30
32
  "dev": "storybook dev -p 6006",
@@ -67,6 +69,7 @@
67
69
  "eslint-config-prettier": "^10.1.8",
68
70
  "eslint-plugin-vue": "^10.7.0",
69
71
  "happy-dom": "^20.5.3",
72
+ "postcss-rtlcss": "^5.7.1",
70
73
  "prettier": "^3.8.1",
71
74
  "quasar": "^2.18.6",
72
75
  "sass-embedded": "^1.97.3",
@@ -0,0 +1,236 @@
1
+ /*
2
+ * Nonsuch Design Tokens
3
+ *
4
+ * CSS custom properties for the Nonsuch component library.
5
+ * All tokens use the `--ns-` prefix to avoid collisions with
6
+ * Quasar's `--q-` namespace and app-level variables.
7
+ *
8
+ * Import in your app:
9
+ * import '@nonsuch/component-library/tokens.css'
10
+ *
11
+ * Current values are PLACEHOLDERS — they will be updated
12
+ * when brand designs are finalised. Token *names* are stable.
13
+ */
14
+
15
+ /* --------------------------------------------------------
16
+ * LIGHT MODE (default)
17
+ * ------------------------------------------------------ */
18
+ :root {
19
+ /* — Brand colours — */
20
+ --ns-color-primary: #3b82f6; /* PLACEHOLDER */
21
+ --ns-color-primary-hover: #2563eb; /* PLACEHOLDER */
22
+ --ns-color-secondary: #8b5cf6; /* PLACEHOLDER */
23
+ --ns-color-secondary-hover: #7c3aed; /* PLACEHOLDER */
24
+ --ns-color-accent: #f59e0b; /* PLACEHOLDER */
25
+ --ns-color-accent-hover: #d97706; /* PLACEHOLDER */
26
+
27
+ /* — Semantic colours — */
28
+ --ns-color-success: #22c55e; /* PLACEHOLDER */
29
+ --ns-color-warning: #f59e0b; /* PLACEHOLDER */
30
+ --ns-color-error: #ef4444; /* PLACEHOLDER */
31
+ --ns-color-info: #3b82f6; /* PLACEHOLDER */
32
+
33
+ /* — Surface / background — */
34
+ --ns-color-background: #ffffff; /* PLACEHOLDER */
35
+ --ns-color-surface: #ffffff; /* PLACEHOLDER */
36
+ --ns-color-surface-variant: #f8fafc; /* PLACEHOLDER */
37
+
38
+ /* — On-colours (text/icons on top of the corresponding colour) — */
39
+ --ns-color-on-primary: #ffffff; /* PLACEHOLDER */
40
+ --ns-color-on-secondary: #ffffff; /* PLACEHOLDER */
41
+ --ns-color-on-accent: #000000; /* PLACEHOLDER */
42
+ --ns-color-on-background: #0f172a; /* PLACEHOLDER */
43
+ --ns-color-on-surface: #0f172a; /* PLACEHOLDER */
44
+
45
+ /* — Neutral scale — */
46
+ --ns-color-neutral-50: #f8fafc; /* PLACEHOLDER */
47
+ --ns-color-neutral-100: #f1f5f9; /* PLACEHOLDER */
48
+ --ns-color-neutral-200: #e2e8f0; /* PLACEHOLDER */
49
+ --ns-color-neutral-300: #cbd5e1; /* PLACEHOLDER */
50
+ --ns-color-neutral-400: #94a3b8; /* PLACEHOLDER */
51
+ --ns-color-neutral-500: #64748b; /* PLACEHOLDER */
52
+ --ns-color-neutral-600: #475569; /* PLACEHOLDER */
53
+ --ns-color-neutral-700: #334155; /* PLACEHOLDER */
54
+ --ns-color-neutral-800: #1e293b; /* PLACEHOLDER */
55
+ --ns-color-neutral-900: #0f172a; /* PLACEHOLDER */
56
+
57
+ /* — Typography — */
58
+ --ns-font-family-text: 'Fixel Text', 'Roboto', sans-serif; /* PLACEHOLDER */
59
+ --ns-font-family-display: 'Fixel Display', 'Roboto', sans-serif; /* PLACEHOLDER */
60
+
61
+ --ns-font-size-xs: 0.75rem; /* 12px — PLACEHOLDER */
62
+ --ns-font-size-sm: 0.875rem; /* 14px — PLACEHOLDER */
63
+ --ns-font-size-md: 1rem; /* 16px — PLACEHOLDER */
64
+ --ns-font-size-lg: 1.125rem; /* 18px — PLACEHOLDER */
65
+ --ns-font-size-xl: 1.25rem; /* 20px — PLACEHOLDER */
66
+ --ns-font-size-2xl: 1.5rem; /* 24px — PLACEHOLDER */
67
+ --ns-font-size-3xl: 1.875rem; /* 30px — PLACEHOLDER */
68
+
69
+ --ns-font-weight-regular: 400; /* PLACEHOLDER */
70
+ --ns-font-weight-medium: 500; /* PLACEHOLDER */
71
+ --ns-font-weight-semibold: 600; /* PLACEHOLDER */
72
+ --ns-font-weight-bold: 700; /* PLACEHOLDER */
73
+
74
+ --ns-line-height-tight: 1.25; /* PLACEHOLDER */
75
+ --ns-line-height-normal: 1.5; /* PLACEHOLDER */
76
+ --ns-line-height-relaxed: 1.75; /* PLACEHOLDER */
77
+
78
+ --ns-letter-spacing-tight: -0.01em; /* PLACEHOLDER */
79
+ --ns-letter-spacing-normal: 0; /* PLACEHOLDER */
80
+ --ns-letter-spacing-wide: 0.02em; /* PLACEHOLDER */
81
+
82
+ /* — Spacing (4px grid) — */
83
+ --ns-space-1: 0.25rem; /* 4px */
84
+ --ns-space-2: 0.5rem; /* 8px */
85
+ --ns-space-3: 0.75rem; /* 12px */
86
+ --ns-space-4: 1rem; /* 16px */
87
+ --ns-space-5: 1.25rem; /* 20px */
88
+ --ns-space-6: 1.5rem; /* 24px */
89
+ --ns-space-8: 2rem; /* 32px */
90
+ --ns-space-10: 2.5rem; /* 40px */
91
+ --ns-space-12: 3rem; /* 48px */
92
+ --ns-space-16: 4rem; /* 64px */
93
+
94
+ /* — Border radius — */
95
+ --ns-radius-none: 0;
96
+ --ns-radius-sm: 0.25rem; /* 4px — PLACEHOLDER */
97
+ --ns-radius-md: 0.5rem; /* 8px — PLACEHOLDER */
98
+ --ns-radius-lg: 0.75rem; /* 12px — PLACEHOLDER */
99
+ --ns-radius-xl: 1rem; /* 16px — PLACEHOLDER */
100
+ --ns-radius-full: 9999px;
101
+
102
+ /* — Shadows / elevation — */
103
+ --ns-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); /* PLACEHOLDER */
104
+ --ns-shadow-md:
105
+ 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); /* PLACEHOLDER */
106
+ --ns-shadow-lg:
107
+ 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); /* PLACEHOLDER */
108
+ --ns-shadow-xl:
109
+ 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); /* PLACEHOLDER */
110
+
111
+ /* — Motion / transitions — */
112
+ --ns-duration-fast: 100ms; /* PLACEHOLDER */
113
+ --ns-duration-normal: 200ms; /* PLACEHOLDER */
114
+ --ns-duration-slow: 400ms; /* PLACEHOLDER */
115
+
116
+ --ns-easing-default: cubic-bezier(0.4, 0, 0.2, 1); /* PLACEHOLDER */
117
+ --ns-easing-in: cubic-bezier(0.4, 0, 1, 1); /* PLACEHOLDER */
118
+ --ns-easing-out: cubic-bezier(0, 0, 0.2, 1); /* PLACEHOLDER */
119
+ --ns-easing-in-out: cubic-bezier(0.4, 0, 0.2, 1); /* PLACEHOLDER */
120
+ }
121
+
122
+ /* --------------------------------------------------------
123
+ * DARK MODE
124
+ *
125
+ * Activated by any of:
126
+ * <html class="dark">
127
+ * <html data-theme="dark">
128
+ * OS-level prefers-color-scheme: dark
129
+ *
130
+ * Only colour tokens change — typography, spacing, radius,
131
+ * and motion are shared between modes.
132
+ * ------------------------------------------------------ */
133
+ :root.dark,
134
+ [data-theme='dark'],
135
+ .q-dark {
136
+ /* — Brand colours (dark) — */
137
+ --ns-color-primary: #60a5fa; /* PLACEHOLDER */
138
+ --ns-color-primary-hover: #93c5fd; /* PLACEHOLDER */
139
+ --ns-color-secondary: #a78bfa; /* PLACEHOLDER */
140
+ --ns-color-secondary-hover: #c4b5fd; /* PLACEHOLDER */
141
+ --ns-color-accent: #fbbf24; /* PLACEHOLDER */
142
+ --ns-color-accent-hover: #fcd34d; /* PLACEHOLDER */
143
+
144
+ /* — Semantic colours (dark) — */
145
+ --ns-color-success: #4ade80; /* PLACEHOLDER */
146
+ --ns-color-warning: #fbbf24; /* PLACEHOLDER */
147
+ --ns-color-error: #f87171; /* PLACEHOLDER */
148
+ --ns-color-info: #60a5fa; /* PLACEHOLDER */
149
+
150
+ /* — Surface / background (dark) — */
151
+ --ns-color-background: #0f172a; /* PLACEHOLDER */
152
+ --ns-color-surface: #1e293b; /* PLACEHOLDER */
153
+ --ns-color-surface-variant: #334155; /* PLACEHOLDER */
154
+
155
+ /* — On-colours (dark) — */
156
+ --ns-color-on-primary: #0f172a; /* PLACEHOLDER */
157
+ --ns-color-on-secondary: #0f172a; /* PLACEHOLDER */
158
+ --ns-color-on-accent: #0f172a; /* PLACEHOLDER */
159
+ --ns-color-on-background: #f1f5f9; /* PLACEHOLDER */
160
+ --ns-color-on-surface: #f1f5f9; /* PLACEHOLDER */
161
+
162
+ /* — Neutral scale (dark — inverted) — */
163
+ --ns-color-neutral-50: #0f172a; /* PLACEHOLDER */
164
+ --ns-color-neutral-100: #1e293b; /* PLACEHOLDER */
165
+ --ns-color-neutral-200: #334155; /* PLACEHOLDER */
166
+ --ns-color-neutral-300: #475569; /* PLACEHOLDER */
167
+ --ns-color-neutral-400: #64748b; /* PLACEHOLDER */
168
+ --ns-color-neutral-500: #94a3b8; /* PLACEHOLDER */
169
+ --ns-color-neutral-600: #cbd5e1; /* PLACEHOLDER */
170
+ --ns-color-neutral-700: #e2e8f0; /* PLACEHOLDER */
171
+ --ns-color-neutral-800: #f1f5f9; /* PLACEHOLDER */
172
+ --ns-color-neutral-900: #f8fafc; /* PLACEHOLDER */
173
+
174
+ /* — Shadows (dark — lighter opacity looks better on dark bg) — */
175
+ --ns-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3); /* PLACEHOLDER */
176
+ --ns-shadow-md:
177
+ 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3); /* PLACEHOLDER */
178
+ --ns-shadow-lg:
179
+ 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3); /* PLACEHOLDER */
180
+ --ns-shadow-xl:
181
+ 0 20px 25px -5px rgb(0 0 0 / 0.4), 0 8px 10px -6px rgb(0 0 0 / 0.3); /* PLACEHOLDER */
182
+ }
183
+
184
+ /* --------------------------------------------------------
185
+ * prefers-color-scheme fallback
186
+ *
187
+ * Applies dark tokens when the OS is in dark mode and
188
+ * no explicit class/attribute override is set.
189
+ * ------------------------------------------------------ */
190
+ @media (prefers-color-scheme: dark) {
191
+ :root:not([data-theme='light']):not(.light) {
192
+ /* — Brand colours (dark) — */
193
+ --ns-color-primary: #60a5fa;
194
+ --ns-color-primary-hover: #93c5fd;
195
+ --ns-color-secondary: #a78bfa;
196
+ --ns-color-secondary-hover: #c4b5fd;
197
+ --ns-color-accent: #fbbf24;
198
+ --ns-color-accent-hover: #fcd34d;
199
+
200
+ /* — Semantic colours (dark) — */
201
+ --ns-color-success: #4ade80;
202
+ --ns-color-warning: #fbbf24;
203
+ --ns-color-error: #f87171;
204
+ --ns-color-info: #60a5fa;
205
+
206
+ /* — Surface / background (dark) — */
207
+ --ns-color-background: #0f172a;
208
+ --ns-color-surface: #1e293b;
209
+ --ns-color-surface-variant: #334155;
210
+
211
+ /* — On-colours (dark) — */
212
+ --ns-color-on-primary: #0f172a;
213
+ --ns-color-on-secondary: #0f172a;
214
+ --ns-color-on-accent: #0f172a;
215
+ --ns-color-on-background: #f1f5f9;
216
+ --ns-color-on-surface: #f1f5f9;
217
+
218
+ /* — Neutral scale (dark — inverted) — */
219
+ --ns-color-neutral-50: #0f172a;
220
+ --ns-color-neutral-100: #1e293b;
221
+ --ns-color-neutral-200: #334155;
222
+ --ns-color-neutral-300: #475569;
223
+ --ns-color-neutral-400: #64748b;
224
+ --ns-color-neutral-500: #94a3b8;
225
+ --ns-color-neutral-600: #cbd5e1;
226
+ --ns-color-neutral-700: #e2e8f0;
227
+ --ns-color-neutral-800: #f1f5f9;
228
+ --ns-color-neutral-900: #f8fafc;
229
+
230
+ /* — Shadows (dark) — */
231
+ --ns-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
232
+ --ns-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
233
+ --ns-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3);
234
+ --ns-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.4), 0 8px 10px -6px rgb(0 0 0 / 0.3);
235
+ }
236
+ }