Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/css-reset-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cube-dev/ui-kit": minor
---

Add `cssReset` configuration option that injects a global CSS reset when styles are first generated. The reset includes sensible defaults for box-sizing, margins, typography, and form elements. It's wrapped in an unnamed `@layer` for lowest cascade priority. Disabled by default, can be enabled with `configure({ cssReset: true })`.
1 change: 1 addition & 0 deletions src/stories/Tasty.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ configure({
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `nonce` | `string` | - | CSP nonce for style elements |
| `cssReset` | `boolean` | `false` | Inject a global CSS reset with sensible defaults (box-sizing, margins, typography) |
| `states` | `Record<string, string>` | - | Global state aliases for advanced state mapping |
| `parserCacheSize` | `number` | `1000` | Parser LRU cache size |
| `units` | `Record<string, string \| Function>` | Built-in | Custom units (merged with built-in) |
Expand Down
18 changes: 18 additions & 0 deletions src/tasty/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* reconfigure will emit a warning and be ignored.
*/

import { CSS_RESET } from './css-reset';
import { StyleInjector } from './injector/injector';
import { clearPipelineCache } from './pipeline';
import { setGlobalPredefinedStates } from './states';
Expand All @@ -33,6 +34,9 @@ import type { StyleDetails, UnitHandler } from './parser/types';
import type { TastyPlugin } from './plugins/types';
import type { StyleHandlerDefinition } from './utils/styles';

// Re-export CSS_RESET for external use
export { CSS_RESET };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused re-export of CSS_RESET from config module

Low Severity

The CSS_RESET re-export from config.ts is marked "for external use" but is never included in the package's barrel file (src/tasty/index.ts). The barrel file only exports specific named exports from ./config and doesn't include CSS_RESET. Both internal consumers (config.ts and zero/babel.ts) import directly from ./css-reset, not from the re-export. This makes the re-export dead code that's inaccessible to external users.

Fix in Cursor Fix in Web


/**
* Configuration options for the Tasty style system
*/
Expand Down Expand Up @@ -197,6 +201,13 @@ export interface TastyConfig {
tokens?: {
[key: `$${string}` | `#${string}`]: string | number;
};
/**
* Whether to inject a global CSS reset when styles are first generated.
* The reset includes sensible defaults for box-sizing, margins, typography,
* and form elements. It's wrapped in an unnamed `@layer` for lowest cascade priority.
* @default false
*/
cssReset?: boolean;
}

// Warnings tracking to avoid duplicates
Expand Down Expand Up @@ -302,6 +313,7 @@ function createDefaultConfig(isTest?: boolean): TastyConfig {
devMode: isDevEnv(),
bulkCleanupBatchRatio: 0.5,
unusedStylesMinAgeMs: 10000,
cssReset: false,
};
}

Expand All @@ -321,6 +333,12 @@ export function markStylesGenerated(): void {

const injector = getGlobalInjector();

// Inject CSS reset if enabled (default: false)
const config = getConfig();
if (config.cssReset === true) {
injector.injectRawCSS(CSS_RESET);
}

// Inject internal properties required by tasty core features
for (const [token, definition] of Object.entries(INTERNAL_PROPERTIES)) {
injector.property(token, definition);
Expand Down
146 changes: 146 additions & 0 deletions src/tasty/css-reset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Global CSS reset injected when cssReset is enabled.
* Wrapped in an unnamed @layer for lowest cascade priority.
*/
export const CSS_RESET = `@layer {
*,
*::before,
*::after {
box-sizing: border-box;
background-repeat: no-repeat;
}

* {
padding: 0;
margin: 0;
}

html {
-webkit-text-size-adjust: none;
text-size-adjust: none;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
block-size: 100%;
}

body {
min-block-size: 100%;
}

img,
iframe,
audio,
video,
canvas {
display: block;
max-inline-size: 100%;
block-size: auto;
}

svg {
max-inline-size: 100%;
}

svg:not([fill]) {
fill: currentColor;
}

input,
button,
textarea,
select {
font: inherit;
}

textarea {
resize: vertical;
}

fieldset,
iframe {
border: none;
}

p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}

p {
text-wrap: pretty;
font-variant-numeric: proportional-nums;
}

h1,
h2,
h3,
h4,
h5,
h6 {
font-variant-numeric: lining-nums;
}

p,
blockquote,
q,
figcaption,
li {
hanging-punctuation: first allow-end last;
}

input,
label,
button,
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.2;
}

math,
time,
table {
font-variant-numeric: tabular-nums lining-nums slashed-zero;
}

code {
font-variant-numeric: slashed-zero;
}

table {
border-collapse: collapse;
}

abbr {
font-variant-caps: all-small-caps;
text-decoration: none;
}

abbr[title] {
cursor: help;
text-decoration: underline dotted;
}

sup,
sub {
line-height: 0;
}

:disabled:not([data-disabled]) {
opacity: 0.5;
cursor: not-allowed;
}

:focus-visible {
outline-offset: 0;
}
}
`;
1 change: 0 additions & 1 deletion src/tasty/styles/reset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const RESET_MAP = {
input: [
{
'-webkit-appearance': 'none',
'word-spacing': 'initial',
'-webkit-text-fill-color': 'currentColor',
},
{
Expand Down
13 changes: 13 additions & 0 deletions src/tasty/zero/babel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { declare } from '@babel/helper-plugin-utils';
import * as t from '@babel/types';

import { configure } from '../config';
import { CSS_RESET } from '../css-reset';
import { Styles } from '../styles/types';
import { mergeStyles } from '../utils/merge-styles';
import { StyleHandlerDefinition } from '../utils/styles';
Expand Down Expand Up @@ -116,6 +117,13 @@ export interface TastyZeroConfig {
tokens?: {
[key: `$${string}` | `#${string}`]: string | number;
};
/**
* Whether to include a global CSS reset at the beginning of the generated CSS.
* The reset includes sensible defaults for box-sizing, margins, typography,
* and form elements. It's wrapped in an unnamed `@layer` for lowest cascade priority.
* @default false
*/
cssReset?: boolean;
}

export interface TastyZeroBabelOptions {
Expand Down Expand Up @@ -267,6 +275,11 @@ export default declare<TastyZeroBabelOptions>((api, options) => {
},

post() {
// Prepend CSS reset at the beginning if enabled (default: false)
if (config.cssReset === true) {
cssWriter.prepend('__css_reset__', CSS_RESET);
}

// Write all collected CSS at the end of the build
if (cssWriter.size > 0) {
cssWriter.write();
Expand Down
16 changes: 16 additions & 0 deletions src/tasty/zero/css-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ export class CSSWriter {
this.cssBlocks.set(key, { css, source });
}

/**
* Prepend CSS block at the beginning (before all existing blocks)
* @param key - Unique key for deduplication
* @param css - CSS content
* @param source - Optional source file path (used in devMode)
*/
prepend(key: string, css: string, source?: string): void {
// Create new Map with the prepended entry first
const newBlocks = new Map<string, CSSBlock>();
newBlocks.set(key, { css, source });
for (const [existingKey, existingBlock] of this.cssBlocks) {
newBlocks.set(existingKey, existingBlock);
}
this.cssBlocks = newBlocks;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prepend method overwrites new value with old value

Low Severity

The prepend method has a logic flaw: it first sets the new entry with newBlocks.set(key, { css, source }), then iterates over all existing blocks including any entry with the same key, which overwrites the newly set value with the old one. This doesn't currently manifest because CSS_RESET is a constant and source is always undefined, making old and new values identical. However, the method would fail to update content if called with the same key but different css or source values.

Fix in Cursor Fix in Web


/**
* Check if a key already exists
*/
Expand Down
Loading