@nonsuch/component-library 0.3.0 → 0.5.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,92 @@ 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(
78
+ Quasar,
79
+ createQuasarConfig({
80
+ brand: { primary: '#1a73e8' },
81
+ plugins: { Notify: {} },
82
+ }),
83
+ )
39
84
  ```
40
85
 
41
- Then import custom components as needed:
86
+ ### Dark Mode
42
87
 
43
88
  ```ts
44
- import { NsButton } from '@nonsuch/component-library'
89
+ import { useNsDarkMode } from '@nonsuch/component-library'
90
+
91
+ const { isDark, toggle, useSystem } = useNsDarkMode()
92
+ ```
93
+
94
+ 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>`.
95
+
96
+ ### NsThemeProvider
97
+
98
+ For section-level locale overrides without a plugin:
99
+
100
+ ```vue
101
+ <script setup>
102
+ import { NsThemeProvider, nsLocaleFrCA } from '@nonsuch/component-library'
103
+ </script>
104
+
105
+ <template>
106
+ <NsThemeProvider :locale="nsLocaleFrCA">
107
+ <!-- All Ns components here use French strings -->
108
+ </NsThemeProvider>
109
+ </template>
45
110
  ```
46
111
 
112
+ ### Manual Setup (Advanced)
113
+
47
114
  ### Fonts (Optional)
48
115
 
49
116
  The library ships [Fixel](https://fixel.macpaw.com/) as the Nonsuch brand font with Roboto as a fallback. Three integration options:
@@ -86,6 +153,77 @@ Or use Quasar components directly — they aren't re-exported through this libra
86
153
  import { QInput, QSelect } from 'quasar'
87
154
  ```
88
155
 
156
+ ### Translations (i18n)
157
+
158
+ 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.
159
+
160
+ **Built-in locales:** `en-CA` (default) and `fr-CA`.
161
+
162
+ **Option 1: Use the defaults** — components use English (Canada) strings out of the box with no setup:
163
+
164
+ ```vue
165
+ <NsButton>Add to cart</NsButton>
166
+ <!-- internal labels like loading text already default to English -->
167
+ ```
168
+
169
+ **Option 2: Switch locale globally** — provide a locale pack at the app root:
170
+
171
+ ```ts
172
+ import { createApp } from 'vue'
173
+ import { provideNsLocale, nsLocaleFrCA } from '@nonsuch/component-library'
174
+
175
+ const app = createApp(App)
176
+
177
+ // Inside your root component's setup():
178
+ provideNsLocale(nsLocaleFrCA)
179
+ ```
180
+
181
+ **Option 3: Custom / partial locale** — supply your own translations by implementing the `NsLocaleMessages` interface:
182
+
183
+ ```ts
184
+ import type { NsLocaleMessages } from '@nonsuch/component-library'
185
+ import { nsLocaleEnCA, provideNsLocale } from '@nonsuch/component-library'
186
+
187
+ const myLocale: NsLocaleMessages = {
188
+ ...nsLocaleEnCA,
189
+ product: {
190
+ ...nsLocaleEnCA.product,
191
+ addToCart: 'Add to bag', // override just what you need
192
+ },
193
+ }
194
+
195
+ provideNsLocale(myLocale)
196
+ ```
197
+
198
+ **Option 4: Override per-component** — pass a string prop directly to bypass the locale:
199
+
200
+ ```vue
201
+ <!-- This label is always "Ajouter" regardless of the active locale -->
202
+ <NsButton label="Ajouter" />
203
+ ```
204
+
205
+ The locale interface covers four sections: `common`, `product`, `media`, and `validation`. See the full type in `NsLocaleMessages`.
206
+
207
+ ### Design Tokens (Optional)
208
+
209
+ Import CSS custom properties for colours, typography, spacing, border-radius, shadows, and motion:
210
+
211
+ ```ts
212
+ import '@nonsuch/component-library/tokens.css'
213
+ ```
214
+
215
+ All tokens use the `--ns-` prefix and support light/dark mode automatically. Current values are placeholders — token names are stable.
216
+
217
+ ```css
218
+ .my-card {
219
+ border-radius: var(--ns-radius-md);
220
+ box-shadow: var(--ns-shadow-sm);
221
+ padding: var(--ns-space-4);
222
+ }
223
+ ```
224
+
225
+ Dark mode activates via `class="dark"`, `data-theme="dark"`, Quasar's `.q-dark`, or `prefers-color-scheme: dark`.
226
+
89
227
  ## Development
90
228
 
91
229
  ```bash
@@ -108,15 +246,34 @@ pnpm build:storybook
108
246
 
109
247
  ## Project Structure
110
248
 
111
- ```markdown
249
+ ```text
112
250
  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
251
+ index.ts # Library entry — exports all public API
252
+ plugin.ts # createNonsuch() Vue plugin
253
+ quasarConfig.ts # createQuasarConfig() helper
254
+ components/
255
+ NsAvatar/ # Avatar with size presets
256
+ NsBanner/ # Info/success/warning/error banners
257
+ NsButton/ # Styled QBtn wrapper
258
+ NsCard/ # Card with title/subtitle/actions slots
259
+ NsCheckbox/ # Styled QCheckbox wrapper
260
+ NsChip/ # Tag/filter chip
261
+ NsDialog/ # Modal dialog with header/body/actions
262
+ NsForm/ # Form wrapper with validation
263
+ NsInput/ # Styled QInput wrapper
264
+ NsList/ # List with separator defaults
265
+ NsSelect/ # Styled QSelect dropdown
266
+ NsSkeleton/ # Loading skeleton with animation
267
+ NsThemeProvider/ # Renderless locale provider
268
+ NsToggle/ # Styled QToggle switch
269
+ NsTooltip/ # Tooltip with consistent delays
270
+ composables/
271
+ useNsLocale.ts # Locale injection/provision
272
+ useNsDarkMode.ts # Dark mode with persistence
273
+ useNsDefaults.ts # Default value helper
274
+ locale/ # en-CA, fr-CA string packs
275
+ tokens/ # Design token CSS + TS helpers
276
+ fonts/ # Fixel font files + CSS
120
277
  ```
121
278
 
122
279
  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-ed26f034]{font-family:Fixel Text,Roboto,sans-serif;font-weight:500;letter-spacing:.02em}.ns-skeleton[data-v-6093e4f3]{border-radius:8px}
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)}.ns-select[data-v-081cebe3],.ns-select[data-v-081cebe3] .q-field__label{font-family:var(--ns-font-family-text)}.ns-select[data-v-081cebe3] .q-field__control{border-radius:var(--ns-radius-md)}.ns-checkbox[data-v-2e481f8b],.ns-toggle[data-v-beb96bf2],.ns-form[data-v-0fd74bb1]{font-family:var(--ns-font-family-text)}.ns-dialog__card[data-v-9bff0f12]{border-radius:var(--ns-radius-lg);font-family:var(--ns-font-family-text);min-width:320px}.ns-dialog__header[data-v-9bff0f12]{font-family:var(--ns-font-family-display)}.ns-banner[data-v-35ed7ae7]{font-family:var(--ns-font-family-text);border-radius:var(--ns-radius-md)}.ns-banner--info[data-v-35ed7ae7]{background-color:var(--ns-color-info-bg, #e3f2fd);color:var(--ns-color-info-text, #0d47a1)}.ns-banner--success[data-v-35ed7ae7]{background-color:var(--ns-color-success-bg, #e8f5e9);color:var(--ns-color-success-text, #1b5e20)}.ns-banner--warning[data-v-35ed7ae7]{background-color:var(--ns-color-warning-bg, #fff3e0);color:var(--ns-color-warning-text, #e65100)}.ns-banner--error[data-v-35ed7ae7]{background-color:var(--ns-color-error-bg, #ffebee);color:var(--ns-color-error-text, #b71c1c)}.ns-avatar[data-v-e4df869f]{font-family:var(--ns-font-family-text);font-weight:var(--ns-font-weight-medium)}.ns-chip[data-v-1a2191f1]{font-family:var(--ns-font-family-text);border-radius:var(--ns-radius-full, 9999px)}.ns-list[data-v-b778ff3f]{font-family:var(--ns-font-family-text);border-radius:var(--ns-radius-md)}.ns-tooltip[data-v-9cb6af8f]{font-family:var(--ns-font-family-text);font-size:var(--ns-font-size-sm, .875rem);border-radius:var(--ns-radius-sm);padding:var(--ns-space-1, 4px) var(--ns-space-2, 8px)}
@@ -1,8 +1,22 @@
1
- import h from "quasar/src/components/btn/QBtn.js";
2
- import { defineComponent as s, openBlock as u, createBlock as d, mergeProps as c, withCtx as l, renderSlot as i, createVNode as g, unref as v, provide as y, inject as S, computed as k } from "vue";
3
- import w from "quasar/src/components/spinner/QSpinnerDots.js";
4
- import C from "quasar/src/components/skeleton/QSkeleton.js";
5
- const B = /* @__PURE__ */ s({
1
+ import M from "quasar/src/components/btn/QBtn.js";
2
+ import { defineComponent as u, openBlock as n, createBlock as d, mergeProps as i, withCtx as l, renderSlot as r, createVNode as g, unref as F, provide as U, inject as j, createSlots as k, renderList as Q, normalizeProps as E, guardReactiveProps as z, createElementVNode as L, toDisplayString as $, createElementBlock as K, createCommentVNode as p, computed as T, ref as I, onMounted as Z, onUnmounted as G, readonly as w } from "vue";
3
+ import H from "quasar/src/components/spinner/QSpinnerDots.js";
4
+ import Y from "quasar/src/components/skeleton/QSkeleton.js";
5
+ import J from "quasar/src/components/input/QInput.js";
6
+ import b from "quasar/src/components/card/QCardSection.js";
7
+ import A from "quasar/src/components/card/QCardActions.js";
8
+ import q from "quasar/src/components/card/QCard.js";
9
+ import W from "quasar/src/components/select/QSelect.js";
10
+ import X from "quasar/src/components/checkbox/QCheckbox.js";
11
+ import x from "quasar/src/components/toggle/QToggle.js";
12
+ import _ from "quasar/src/components/form/QForm.js";
13
+ import ee from "quasar/src/components/dialog/QDialog.js";
14
+ import te from "quasar/src/components/banner/QBanner.js";
15
+ import oe from "quasar/src/components/avatar/QAvatar.js";
16
+ import ae from "quasar/src/components/chip/QChip.js";
17
+ import le from "quasar/src/components/item/QList.js";
18
+ import se from "quasar/src/components/tooltip/QTooltip.js";
19
+ const ne = /* @__PURE__ */ u({
6
20
  __name: "NsButton",
7
21
  props: {
8
22
  color: { default: "primary" },
@@ -13,7 +27,7 @@ const B = /* @__PURE__ */ s({
13
27
  loading: { type: Boolean, default: !1 }
14
28
  },
15
29
  setup(e) {
16
- return (o, t) => (u(), d(h, c(o.$attrs, {
30
+ return (t, o) => (n(), d(M, i(t.$attrs, {
17
31
  color: e.color,
18
32
  size: e.size,
19
33
  unelevated: e.unelevated,
@@ -23,22 +37,22 @@ const B = /* @__PURE__ */ s({
23
37
  class: "ns-button"
24
38
  }), {
25
39
  loading: l(() => [
26
- i(o.$slots, "loading", {}, () => [
27
- g(v(w), { color: "white" })
40
+ r(t.$slots, "loading", {}, () => [
41
+ g(F(H), { color: "white" })
28
42
  ], !0)
29
43
  ]),
30
44
  default: l(() => [
31
- i(o.$slots, "default", {}, void 0, !0)
45
+ r(t.$slots, "default", {}, void 0, !0)
32
46
  ]),
33
47
  _: 3
34
48
  }, 16, ["color", "size", "unelevated", "no-caps", "rounded", "loading"]));
35
49
  }
36
- }), m = (e, o) => {
37
- const t = e.__vccOpts || e;
38
- for (const [n, r] of o)
39
- t[n] = r;
40
- return t;
41
- }, E = /* @__PURE__ */ m(B, [["__scopeId", "data-v-ed26f034"]]), I = /* @__PURE__ */ s({
50
+ }), f = (e, t) => {
51
+ const o = e.__vccOpts || e;
52
+ for (const [a, s] of t)
53
+ o[a] = s;
54
+ return o;
55
+ }, Ke = /* @__PURE__ */ f(ne, [["__scopeId", "data-v-7f6c2760"]]), re = /* @__PURE__ */ u({
42
56
  __name: "NsSkeleton",
43
57
  props: {
44
58
  type: { default: "rect" },
@@ -49,7 +63,7 @@ const B = /* @__PURE__ */ s({
49
63
  height: { default: void 0 }
50
64
  },
51
65
  setup(e) {
52
- return (o, t) => (u(), d(C, c(o.$attrs, {
66
+ return (t, o) => (n(), d(Y, i(t.$attrs, {
53
67
  type: e.type,
54
68
  animation: e.animation,
55
69
  square: e.square,
@@ -59,7 +73,7 @@ const B = /* @__PURE__ */ s({
59
73
  class: "ns-skeleton"
60
74
  }), null, 16, ["type", "animation", "square", "bordered", "width", "height"]));
61
75
  }
62
- }), R = /* @__PURE__ */ m(I, [["__scopeId", "data-v-6093e4f3"]]), N = {
76
+ }), Ze = /* @__PURE__ */ f(re, [["__scopeId", "data-v-62627927"]]), B = {
63
77
  common: {
64
78
  loading: "Loading…",
65
79
  retry: "Retry",
@@ -95,7 +109,423 @@ const B = /* @__PURE__ */ s({
95
109
  tooShort: "Too short",
96
110
  tooLong: "Too long"
97
111
  }
98
- }, T = {
112
+ }, N = /* @__PURE__ */ Symbol("ns-locale");
113
+ function de(e) {
114
+ U(N, e);
115
+ }
116
+ function ue() {
117
+ return j(N, B);
118
+ }
119
+ const Ge = /* @__PURE__ */ u({
120
+ __name: "NsThemeProvider",
121
+ props: {
122
+ locale: { default: () => B }
123
+ },
124
+ setup(e) {
125
+ return de(e.locale), (o, a) => r(o.$slots, "default");
126
+ }
127
+ }), ie = /* @__PURE__ */ u({
128
+ __name: "NsInput",
129
+ props: {
130
+ label: { default: void 0 },
131
+ modelValue: { default: void 0 },
132
+ outlined: { type: Boolean, default: !0 },
133
+ dense: { type: Boolean, default: !1 },
134
+ rules: { default: void 0 }
135
+ },
136
+ emits: ["update:modelValue"],
137
+ setup(e) {
138
+ return (t, o) => (n(), d(J, i(t.$attrs, {
139
+ "model-value": e.modelValue,
140
+ label: e.label,
141
+ outlined: e.outlined,
142
+ dense: e.dense,
143
+ rules: e.rules,
144
+ class: "ns-input",
145
+ "onUpdate:modelValue": o[0] || (o[0] = (a) => t.$emit("update:modelValue", a))
146
+ }), k({ _: 2 }, [
147
+ Q(t.$slots, (a, s) => ({
148
+ name: s,
149
+ fn: l((c) => [
150
+ r(t.$slots, s, E(z(c ?? {})), void 0, !0)
151
+ ])
152
+ }))
153
+ ]), 1040, ["model-value", "label", "outlined", "dense", "rules"]));
154
+ }
155
+ }), He = /* @__PURE__ */ f(ie, [["__scopeId", "data-v-7ebf3284"]]), fe = { class: "text-h6" }, ce = {
156
+ key: 0,
157
+ class: "text-subtitle2 text-grey"
158
+ }, me = /* @__PURE__ */ u({
159
+ __name: "NsCard",
160
+ props: {
161
+ title: { default: void 0 },
162
+ subtitle: { default: void 0 },
163
+ flat: { type: Boolean, default: !1 }
164
+ },
165
+ setup(e) {
166
+ return (t, o) => (n(), d(q, i(t.$attrs, {
167
+ class: ["ns-card", { "ns-card--flat": e.flat }]
168
+ }), {
169
+ default: l(() => [
170
+ e.title || t.$slots.header ? (n(), d(b, {
171
+ key: 0,
172
+ class: "ns-card__header"
173
+ }, {
174
+ default: l(() => [
175
+ r(t.$slots, "header", {}, () => [
176
+ L("div", fe, $(e.title), 1),
177
+ e.subtitle ? (n(), K("div", ce, $(e.subtitle), 1)) : p("", !0)
178
+ ], !0)
179
+ ]),
180
+ _: 3
181
+ })) : p("", !0),
182
+ g(b, null, {
183
+ default: l(() => [
184
+ r(t.$slots, "default", {}, void 0, !0)
185
+ ]),
186
+ _: 3
187
+ }),
188
+ t.$slots.actions ? (n(), d(A, {
189
+ key: 1,
190
+ align: "right"
191
+ }, {
192
+ default: l(() => [
193
+ r(t.$slots, "actions", {}, void 0, !0)
194
+ ]),
195
+ _: 3
196
+ })) : p("", !0)
197
+ ]),
198
+ _: 3
199
+ }, 16, ["class"]));
200
+ }
201
+ }), Ye = /* @__PURE__ */ f(me, [["__scopeId", "data-v-41ecea50"]]), pe = /* @__PURE__ */ u({
202
+ __name: "NsSelect",
203
+ props: {
204
+ label: { default: void 0 },
205
+ modelValue: { default: void 0 },
206
+ options: { default: () => [] },
207
+ outlined: { type: Boolean, default: !0 },
208
+ dense: { type: Boolean, default: !1 },
209
+ multiple: { type: Boolean, default: !1 },
210
+ emitValue: { type: Boolean, default: !1 },
211
+ mapOptions: { type: Boolean, default: !1 },
212
+ rules: { default: void 0 }
213
+ },
214
+ emits: ["update:modelValue"],
215
+ setup(e) {
216
+ return (t, o) => (n(), d(W, i(t.$attrs, {
217
+ "model-value": e.modelValue,
218
+ label: e.label,
219
+ options: e.options,
220
+ outlined: e.outlined,
221
+ dense: e.dense,
222
+ rules: e.rules,
223
+ multiple: e.multiple,
224
+ "emit-value": e.emitValue,
225
+ "map-options": e.mapOptions,
226
+ class: "ns-select",
227
+ "onUpdate:modelValue": o[0] || (o[0] = (a) => t.$emit("update:modelValue", a))
228
+ }), k({ _: 2 }, [
229
+ Q(t.$slots, (a, s) => ({
230
+ name: s,
231
+ fn: l((c) => [
232
+ r(t.$slots, s, E(z(c ?? {})), void 0, !0)
233
+ ])
234
+ }))
235
+ ]), 1040, ["model-value", "label", "options", "outlined", "dense", "rules", "multiple", "emit-value", "map-options"]));
236
+ }
237
+ }), Je = /* @__PURE__ */ f(pe, [["__scopeId", "data-v-081cebe3"]]), ve = /* @__PURE__ */ u({
238
+ __name: "NsCheckbox",
239
+ props: {
240
+ label: { default: void 0 },
241
+ modelValue: { type: Boolean, default: !1 },
242
+ color: { default: "primary" },
243
+ dense: { type: Boolean, default: !1 },
244
+ disable: { type: Boolean, default: !1 }
245
+ },
246
+ emits: ["update:modelValue"],
247
+ setup(e) {
248
+ return (t, o) => (n(), d(X, i(t.$attrs, {
249
+ "model-value": e.modelValue,
250
+ label: e.label,
251
+ color: e.color,
252
+ dense: e.dense,
253
+ disable: e.disable,
254
+ class: "ns-checkbox",
255
+ "onUpdate:modelValue": o[0] || (o[0] = (a) => t.$emit("update:modelValue", a))
256
+ }), null, 16, ["model-value", "label", "color", "dense", "disable"]));
257
+ }
258
+ }), We = /* @__PURE__ */ f(ve, [["__scopeId", "data-v-2e481f8b"]]), ye = /* @__PURE__ */ u({
259
+ __name: "NsToggle",
260
+ props: {
261
+ label: { default: void 0 },
262
+ modelValue: { type: Boolean, default: !1 },
263
+ color: { default: "primary" },
264
+ dense: { type: Boolean, default: !1 },
265
+ disable: { type: Boolean, default: !1 }
266
+ },
267
+ emits: ["update:modelValue"],
268
+ setup(e) {
269
+ return (t, o) => (n(), d(x, i(t.$attrs, {
270
+ "model-value": e.modelValue,
271
+ label: e.label,
272
+ color: e.color,
273
+ dense: e.dense,
274
+ disable: e.disable,
275
+ class: "ns-toggle",
276
+ "onUpdate:modelValue": o[0] || (o[0] = (a) => t.$emit("update:modelValue", a))
277
+ }), null, 16, ["model-value", "label", "color", "dense", "disable"]));
278
+ }
279
+ }), Xe = /* @__PURE__ */ f(ye, [["__scopeId", "data-v-beb96bf2"]]), ge = /* @__PURE__ */ u({
280
+ __name: "NsForm",
281
+ props: {
282
+ greedy: { type: Boolean, default: !0 }
283
+ },
284
+ emits: ["submit", "validationError"],
285
+ setup(e) {
286
+ return (t, o) => (n(), d(_, i(t.$attrs, {
287
+ class: "ns-form",
288
+ greedy: e.greedy,
289
+ onSubmit: o[0] || (o[0] = (a) => t.$emit("submit", a)),
290
+ onValidationError: o[1] || (o[1] = (a) => t.$emit("validationError", a))
291
+ }), {
292
+ default: l(() => [
293
+ r(t.$slots, "default", {}, void 0, !0)
294
+ ]),
295
+ _: 3
296
+ }, 16, ["greedy"]));
297
+ }
298
+ }), xe = /* @__PURE__ */ f(ge, [["__scopeId", "data-v-0fd74bb1"]]), be = { class: "text-h6" }, he = /* @__PURE__ */ u({
299
+ __name: "NsDialog",
300
+ props: {
301
+ modelValue: { type: Boolean, default: !1 },
302
+ title: { default: void 0 },
303
+ persistent: { type: Boolean, default: !1 },
304
+ noBackdropDismiss: { type: Boolean, default: !1 }
305
+ },
306
+ emits: ["update:modelValue"],
307
+ setup(e) {
308
+ return (t, o) => (n(), d(ee, i(t.$attrs, {
309
+ "model-value": e.modelValue,
310
+ persistent: e.persistent,
311
+ "no-backdrop-dismiss": e.noBackdropDismiss,
312
+ class: "ns-dialog",
313
+ "onUpdate:modelValue": o[0] || (o[0] = (a) => t.$emit("update:modelValue", a))
314
+ }), {
315
+ default: l(() => [
316
+ g(q, { class: "ns-dialog__card" }, {
317
+ default: l(() => [
318
+ e.title || t.$slots.header ? (n(), d(b, {
319
+ key: 0,
320
+ class: "ns-dialog__header"
321
+ }, {
322
+ default: l(() => [
323
+ r(t.$slots, "header", {}, () => [
324
+ L("div", be, $(e.title), 1)
325
+ ], !0)
326
+ ]),
327
+ _: 3
328
+ })) : p("", !0),
329
+ g(b, { class: "ns-dialog__body" }, {
330
+ default: l(() => [
331
+ r(t.$slots, "default", {}, void 0, !0)
332
+ ]),
333
+ _: 3
334
+ }),
335
+ t.$slots.actions ? (n(), d(A, {
336
+ key: 1,
337
+ align: "right",
338
+ class: "ns-dialog__actions"
339
+ }, {
340
+ default: l(() => [
341
+ r(t.$slots, "actions", {}, void 0, !0)
342
+ ]),
343
+ _: 3
344
+ })) : p("", !0)
345
+ ]),
346
+ _: 3
347
+ })
348
+ ]),
349
+ _: 3
350
+ }, 16, ["model-value", "persistent", "no-backdrop-dismiss"]));
351
+ }
352
+ }), _e = /* @__PURE__ */ f(he, [["__scopeId", "data-v-9bff0f12"]]), $e = /* @__PURE__ */ u({
353
+ __name: "NsBanner",
354
+ props: {
355
+ type: { default: "info" },
356
+ dense: { type: Boolean, default: !1 },
357
+ rounded: { type: Boolean, default: !0 }
358
+ },
359
+ setup(e) {
360
+ return (t, o) => (n(), d(te, i(t.$attrs, {
361
+ class: ["ns-banner", `ns-banner--${e.type}`],
362
+ dense: e.dense,
363
+ rounded: e.rounded
364
+ }), k({
365
+ default: l(() => [
366
+ r(t.$slots, "default", {}, void 0, !0)
367
+ ]),
368
+ _: 2
369
+ }, [
370
+ t.$slots.avatar ? {
371
+ name: "avatar",
372
+ fn: l(() => [
373
+ r(t.$slots, "avatar", {}, void 0, !0)
374
+ ]),
375
+ key: "0"
376
+ } : void 0,
377
+ t.$slots.action ? {
378
+ name: "action",
379
+ fn: l(() => [
380
+ r(t.$slots, "action", {}, void 0, !0)
381
+ ]),
382
+ key: "1"
383
+ } : void 0
384
+ ]), 1040, ["class", "dense", "rounded"]));
385
+ }
386
+ }), et = /* @__PURE__ */ f($e, [["__scopeId", "data-v-35ed7ae7"]]), ke = /* @__PURE__ */ u({
387
+ __name: "NsAvatar",
388
+ props: {
389
+ size: { default: "md" },
390
+ color: { default: "primary" },
391
+ textColor: { default: "white" },
392
+ rounded: { type: Boolean, default: !1 },
393
+ square: { type: Boolean, default: !1 }
394
+ },
395
+ setup(e) {
396
+ const t = {
397
+ sm: "32px",
398
+ md: "48px",
399
+ lg: "64px",
400
+ xl: "96px"
401
+ }, o = e, a = T(() => t[o.size] ?? o.size);
402
+ return (s, c) => (n(), d(oe, i(s.$attrs, {
403
+ size: a.value,
404
+ color: e.color,
405
+ "text-color": e.textColor,
406
+ rounded: e.rounded,
407
+ square: e.square,
408
+ class: "ns-avatar"
409
+ }), {
410
+ default: l(() => [
411
+ r(s.$slots, "default", {}, void 0, !0)
412
+ ]),
413
+ _: 3
414
+ }, 16, ["size", "color", "text-color", "rounded", "square"]));
415
+ }
416
+ }), tt = /* @__PURE__ */ f(ke, [["__scopeId", "data-v-e4df869f"]]), Be = /* @__PURE__ */ u({
417
+ __name: "NsChip",
418
+ props: {
419
+ color: { default: "primary" },
420
+ textColor: { default: "white" },
421
+ outline: { type: Boolean, default: !1 },
422
+ dense: { type: Boolean, default: !1 },
423
+ removable: { type: Boolean, default: !1 },
424
+ clickable: { type: Boolean, default: !1 }
425
+ },
426
+ emits: ["remove"],
427
+ setup(e) {
428
+ return (t, o) => (n(), d(ae, i(t.$attrs, {
429
+ color: e.color,
430
+ "text-color": e.textColor,
431
+ outline: e.outline,
432
+ dense: e.dense,
433
+ removable: e.removable,
434
+ clickable: e.clickable,
435
+ class: "ns-chip",
436
+ onRemove: o[0] || (o[0] = (a) => t.$emit("remove"))
437
+ }), {
438
+ default: l(() => [
439
+ r(t.$slots, "default", {}, void 0, !0)
440
+ ]),
441
+ _: 3
442
+ }, 16, ["color", "text-color", "outline", "dense", "removable", "clickable"]));
443
+ }
444
+ }), ot = /* @__PURE__ */ f(Be, [["__scopeId", "data-v-1a2191f1"]]), Ne = /* @__PURE__ */ u({
445
+ __name: "NsList",
446
+ props: {
447
+ bordered: { type: Boolean, default: !1 },
448
+ separator: { type: Boolean, default: !0 },
449
+ dense: { type: Boolean, default: !1 }
450
+ },
451
+ setup(e) {
452
+ return (t, o) => (n(), d(le, i(t.$attrs, {
453
+ bordered: e.bordered,
454
+ separator: e.separator,
455
+ dense: e.dense,
456
+ class: "ns-list"
457
+ }), {
458
+ default: l(() => [
459
+ r(t.$slots, "default", {}, void 0, !0)
460
+ ]),
461
+ _: 3
462
+ }, 16, ["bordered", "separator", "dense"]));
463
+ }
464
+ }), at = /* @__PURE__ */ f(Ne, [["__scopeId", "data-v-b778ff3f"]]), Se = /* @__PURE__ */ u({
465
+ __name: "NsTooltip",
466
+ props: {
467
+ delay: { default: 300 },
468
+ offset: { default: () => [8, 0] },
469
+ anchor: { default: "bottom middle" },
470
+ self: { default: "top middle" }
471
+ },
472
+ setup(e) {
473
+ return (t, o) => (n(), d(se, i(t.$attrs, {
474
+ delay: e.delay,
475
+ offset: e.offset,
476
+ anchor: e.anchor,
477
+ self: e.self,
478
+ class: "ns-tooltip"
479
+ }), {
480
+ default: l(() => [
481
+ r(t.$slots, "default", {}, void 0, !0)
482
+ ]),
483
+ _: 3
484
+ }, 16, ["delay", "offset", "anchor", "self"]));
485
+ }
486
+ }), lt = /* @__PURE__ */ f(Se, [["__scopeId", "data-v-9cb6af8f"]]);
487
+ function st(e = {}) {
488
+ const { locale: t = B } = e;
489
+ return {
490
+ install(o) {
491
+ o.provide(N, t);
492
+ }
493
+ };
494
+ }
495
+ const Ve = {
496
+ primary: "#3b82f6",
497
+ // PLACEHOLDER — matches --ns-color-primary
498
+ secondary: "#8b5cf6",
499
+ // PLACEHOLDER — matches --ns-color-secondary
500
+ accent: "#f59e0b",
501
+ // PLACEHOLDER — matches --ns-color-accent
502
+ dark: "#1e293b",
503
+ // PLACEHOLDER — matches --ns-color-neutral-800
504
+ "dark-page": "#0f172a",
505
+ // PLACEHOLDER — matches --ns-color-neutral-900
506
+ positive: "#22c55e",
507
+ // PLACEHOLDER — matches --ns-color-success
508
+ negative: "#ef4444",
509
+ // PLACEHOLDER — matches --ns-color-error
510
+ info: "#3b82f6",
511
+ // PLACEHOLDER — matches --ns-color-info
512
+ warning: "#f59e0b"
513
+ // PLACEHOLDER — matches --ns-color-warning
514
+ };
515
+ function nt(e = {}) {
516
+ const { brand: t = {}, plugins: o = {}, ...a } = e;
517
+ return {
518
+ config: {
519
+ brand: {
520
+ ...Ve,
521
+ ...t
522
+ }
523
+ },
524
+ plugins: o,
525
+ ...a
526
+ };
527
+ }
528
+ const rt = {
99
529
  common: {
100
530
  loading: "Chargement…",
101
531
  retry: "Réessayer",
@@ -131,32 +561,99 @@ const B = /* @__PURE__ */ s({
131
561
  tooShort: "Trop court",
132
562
  tooLong: "Trop long"
133
563
  }
134
- }, f = /* @__PURE__ */ Symbol("ns-locale");
135
- function b(e) {
136
- y(f, e);
564
+ };
565
+ function dt(e, t) {
566
+ const o = ue();
567
+ return T(() => {
568
+ const a = e();
569
+ if (a != null) return a;
570
+ const s = t.split(".");
571
+ let c = o;
572
+ for (const v of s)
573
+ c = c[v];
574
+ return c;
575
+ });
137
576
  }
138
- function q() {
139
- return S(f, N);
577
+ const h = "ns-dark-mode";
578
+ function ut() {
579
+ const e = I(!1), t = I("system");
580
+ let o = null, a = null;
581
+ function s(m) {
582
+ if (e.value = m, typeof document < "u") {
583
+ const y = document.documentElement;
584
+ y.classList.toggle("dark", m), y.setAttribute("data-theme", m ? "dark" : "light");
585
+ }
586
+ }
587
+ function c() {
588
+ return typeof window > "u" ? !1 : window.matchMedia("(prefers-color-scheme: dark)").matches;
589
+ }
590
+ function v() {
591
+ if (typeof localStorage > "u") return null;
592
+ const m = localStorage.getItem(h);
593
+ return m === "true" ? !0 : m === "false" ? !1 : null;
594
+ }
595
+ function S(m) {
596
+ typeof localStorage < "u" && localStorage.setItem(h, String(m));
597
+ }
598
+ function D() {
599
+ typeof localStorage < "u" && localStorage.removeItem(h);
600
+ }
601
+ function V() {
602
+ t.value = "user", S(!0), s(!0);
603
+ }
604
+ function C() {
605
+ t.value = "user", S(!1), s(!1);
606
+ }
607
+ function P() {
608
+ e.value ? C() : V();
609
+ }
610
+ function R() {
611
+ D(), t.value = "system", s(c());
612
+ }
613
+ function O() {
614
+ const m = v();
615
+ m !== null ? (t.value = "storage", s(m)) : (t.value = "system", s(c())), typeof window < "u" && (o = window.matchMedia("(prefers-color-scheme: dark)"), a = (y) => {
616
+ v() === null && (t.value = "system", s(y.matches));
617
+ }, o.addEventListener("change", a));
618
+ }
619
+ return Z(O), G(() => {
620
+ o && a && o.removeEventListener("change", a);
621
+ }), {
622
+ isDark: w(e),
623
+ source: w(t),
624
+ enable: V,
625
+ disable: C,
626
+ toggle: P,
627
+ useSystem: R
628
+ };
140
629
  }
141
- function O(e, o) {
142
- const t = q();
143
- return k(() => {
144
- const n = e();
145
- if (n != null) return n;
146
- const r = o.split(".");
147
- let a = t;
148
- for (const p of r)
149
- a = a[p];
150
- return a;
151
- });
630
+ function it(e, t = document.documentElement) {
631
+ return getComputedStyle(t).getPropertyValue(e).trim();
152
632
  }
153
633
  export {
154
- E as NsButton,
155
- f as NsLocaleKey,
156
- R as NsSkeleton,
157
- N as nsLocaleEnCA,
158
- T as nsLocaleFrCA,
159
- b as provideNsLocale,
160
- O as useNsDefault,
161
- q as useNsLocale
634
+ tt as NsAvatar,
635
+ et as NsBanner,
636
+ Ke as NsButton,
637
+ Ye as NsCard,
638
+ We as NsCheckbox,
639
+ ot as NsChip,
640
+ _e as NsDialog,
641
+ xe as NsForm,
642
+ He as NsInput,
643
+ at as NsList,
644
+ N as NsLocaleKey,
645
+ Je as NsSelect,
646
+ Ze as NsSkeleton,
647
+ Ge as NsThemeProvider,
648
+ Xe as NsToggle,
649
+ lt as NsTooltip,
650
+ st as createNonsuch,
651
+ nt as createQuasarConfig,
652
+ it as getToken,
653
+ B as nsLocaleEnCA,
654
+ rt as nsLocaleFrCA,
655
+ de as provideNsLocale,
656
+ ut as useNsDarkMode,
657
+ dt as useNsDefault,
658
+ ue as useNsLocale
162
659
  };
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.3.0",
3
+ "version": "0.5.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",
@@ -39,7 +41,8 @@
39
41
  "test:watch": "vitest",
40
42
  "test:coverage": "vitest run --coverage",
41
43
  "preview": "vite preview",
42
- "prepublishOnly": "pnpm lint && pnpm typecheck && pnpm test && pnpm build"
44
+ "prepublishOnly": "pnpm lint && pnpm typecheck && pnpm test && pnpm build",
45
+ "prepare": "husky"
43
46
  },
44
47
  "peerDependencies": {
45
48
  "@quasar/vite-plugin": "^1.8.0",
@@ -62,11 +65,14 @@
62
65
  "@quasar/vite-plugin": "^1.10.0",
63
66
  "@storybook/vue3-vite": "^10.2.7",
64
67
  "@vitejs/plugin-vue": "^6.0.4",
68
+ "@vitest/coverage-v8": "^4.0.18",
65
69
  "@vue/test-utils": "^2.4.6",
66
- "eslint": "^9.39.2",
70
+ "eslint": "^10.0.0",
67
71
  "eslint-config-prettier": "^10.1.8",
68
72
  "eslint-plugin-vue": "^10.7.0",
69
73
  "happy-dom": "^20.5.3",
74
+ "husky": "^9.1.7",
75
+ "lint-staged": "^16.2.7",
70
76
  "postcss-rtlcss": "^5.7.1",
71
77
  "prettier": "^3.8.1",
72
78
  "quasar": "^2.18.6",
@@ -79,5 +85,9 @@
79
85
  "vue": "^3.5.27",
80
86
  "vue-tsc": "^3.2.4"
81
87
  },
82
- "packageManager": "pnpm@10.29.2"
88
+ "packageManager": "pnpm@10.29.2",
89
+ "lint-staged": {
90
+ "*.{ts,vue}": "eslint --fix",
91
+ "*.{ts,vue,json,md,css,scss,sass,html,yaml,yml}": "prettier --write"
92
+ }
83
93
  }
@@ -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
+ }